Konstantin Lepeshenkov 2021-11-25 22:53:11 +01:00
Родитель e6c38dc3c4
Коммит 0e35a35c01
261 изменённых файлов: 48420 добавлений и 373 удалений

351
.gitignore поставляемый
Просмотреть файл

@ -1,350 +1 @@
## 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
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options 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
nunit-*.xml
# 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/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.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
# 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
*.nupkg
# NuGet Symbol Packages
*.snupkg
# 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
*.appxbundle
*.appxupload
# 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
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# 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/
# CodeRush personal settings
.cr/personal
# 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
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
.vs

150
README.md
Просмотреть файл

@ -1,14 +1,148 @@
# Project
![logo](https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/main-page.png)
# Durable Functions Monitor
> This repo has been populated by an initial template to help get you started. Please
> make sure to update the content to build a great experience for community-building.
A monitoring/debugging UI tool for Azure Durable Functions
As the maintainer of this project, please make a few updates:
[Azure Durable Functions](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview) provide an easy and elegant way of building cloud-native Reliable Stateful Services in the Serverless world. The only thing that's missing so far is a UI for monitoring, managing and debugging your orchestration instances. This project tries to bridge the gap.
- Improving this README.MD file to provide a great experience
- Updating SUPPORT.MD with content about this project's support experience
- Understanding the security reporting process in SECURITY.MD
- Remove this section from the README
[<img alt="Nuget" src="https://img.shields.io/nuget/v/DurableFunctionsMonitor.DotNetBackend?label=current%20version">](https://www.nuget.org/profiles/durablefunctionsmonitor) <img src="https://dev.azure.com/kolepes/DurableFunctionsMonitor/_apis/build/status/DurableFunctionsMonitor-CI-from-yml?branchName=master"/> <img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/scale-tone/DurableFunctionsMonitor?style=flat">
[<img alt="Visual Studio Marketplace Installs" src="https://img.shields.io/visual-studio-marketplace/i/DurableFunctionsMonitor.DurableFunctionsMonitor?label=VsCode%20Extension%20Installs">](https://marketplace.visualstudio.com/items?itemName=DurableFunctionsMonitor.durablefunctionsmonitor) [<img src="https://img.shields.io/docker/pulls/scaletone/durablefunctionsmonitor"/>](https://hub.docker.com/r/scaletone/durablefunctionsmonitor) [<img alt="Nuget" src="https://img.shields.io/nuget/dt/DurableFunctionsMonitor.DotNetBackend?label=nuget%20downloads">](https://www.nuget.org/profiles/durablefunctionsmonitor)
# Prerequisites
To run this on your devbox you need to have [Azure Functions Core Tools](https://www.npmjs.com/package/azure-functions-core-tools) **globally** installed (which is normally already the case, if you're working with Azure Functions - just ensure that you have the latest version of it).
**OR**
[Docker Desktop](https://www.docker.com/products/docker-desktop), if you prefer to run it locally [as a container](https://hub.docker.com/r/scaletone/durablefunctionsmonitor).
# How to run
As a [VsCode Extension](https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/durablefunctionsmonitor-vscodeext/README.md#durable-functions-monitor-as-a-vscode-extension).
* Install it [from the Marketplace](https://marketplace.visualstudio.com/items?itemName=DurableFunctionsMonitor.durablefunctionsmonitor) or from [a VSIX-file](https://github.com/scale-tone/DurableFunctionsMonitor/releases).
* (if you have [Azure Functions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) extension also installed) Goto **Azure Functions** <img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-functions-view-container-icon.png" width="32"> View Container, observe all your TaskHubs under **DURABLE FUNCTIONS** tab and click on them to connect.
* (if not) Type `Durable Functions Monitor` in your Command Palette and then confirm or provide Storage Connection String and Hub Name.
**OR**
[As a standalone service](https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/durablefunctionsmonitor.dotnetbackend/README.md#durablefunctionsmonitordotnetbackend), either running locally on your devbox or deployed into Azure: [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fscale-tone%2FDurableFunctionsMonitor%2Fmaster%2Fdurablefunctionsmonitor.dotnetbackend%2Farm-template.json)
**OR**
[Install it as a NuGet package](https://www.nuget.org/packages/DurableFunctionsMonitor.DotNetBackend) into your own Functions project (.Net Core only).
# Features
## 1. View the list of your Orchestrations and/or Durable Entities, with sorting, infinite scrolling and auto-refresh:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/orchestrations.png" width="882">
## 2. Filter by time range and column values:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/orchestrations-filtered.png" width="882">
## 3. Visualize the filtered list of instances as a Time Histogram or as a Gantt chart:
<img src="https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/readme/screenshots/time-histogram.png" width="700">
## 4. Start new orchestration instances:
<img width="300px" src="https://user-images.githubusercontent.com/5447190/131139060-eb06ef4d-2cc2-48ff-932c-c227f28f1f36.png"/>
<img width="300px" src="https://user-images.githubusercontent.com/5447190/130657962-c1c32575-c82c-4e29-ad88-3951eb821fe8.png"/>
<img width="500px" src="https://user-images.githubusercontent.com/5447190/130658737-e51e259d-e7ec-43a2-902b-79907936fb82.png"/>
## 5. Monitor the status of a certain instance:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/orchestration-details.png" width="882">
## 6. Quickly navigate to a certain instance by its ID:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/goto-instance.png" width="400">
## 7. Observe Sequence Diagrams and Gantt Charts for orchestrations:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestration-diagram.png" width="400">
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/gantt-chart.png" width="650">
## 8. Restart, Purge, Rewind, Terminate, Raise Events, Set Custom Status:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/orchestration-raise-event.png" width="440">
## 9. Purge Orchestration/Entity instances history:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/purge-history-menu.png" width="390">
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/purge-history-dialog.png" width="683">
## 10. Clean deleted Durable Entities:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/clean-entity-storage-menu.png" width="390">
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/clean-entity-storage-dialog.png" width="580">
## 11. Create custom Orchestration/Entity status tabs with [Liquid Templates](https://shopify.github.io/liquid/):
1. Create a [Liquid](https://shopify.github.io/liquid/) template file and name it like `[My Custom Tab Name].[orchestration-or-entity-name].liquid` or just `[My Custom Tab Name].liquid` (this one will be applied to any kind of entity).
2. In the same Storage Account (the account where your Durable Functions run in) create a Blob container called `durable-functions-monitor`.
3. Put your template file into a `tab-templates` virtual folder in that container (the full path should look like `/durable-functions-monitor/tab-templates/[My Custom Tab Name].[orchestration-or-entity-name].liquid`).
4. Restart Durable Functions Monitor.
5. Observe the newly appeared `My Custom Tab Name` tab on the Orchestration/Entity Details page:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/custom-liquid-tab.png" width="390">
Sample Liquid Template:
```
<h2>These people were invited:</h2>
<ul>
{% for participant in Input.Participants %}
<li><h3>{{participant}}<h3></li>
{% endfor %}
</ul>
```
You can have multiple templates for each Orchestration/Entity type, and also multiple 'common' (applied to any Orchestration/Entity) templates.
Here is [a couple](https://gist.github.com/scale-tone/13956ec804a70f5f66200c6ec97db673) [of more](https://github.com/scale-tone/repka-durable-func/blob/master/Repka%20Status.the-saga-of-repka.liquid) sample templates.
NOTE1: [this .Net object](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.durableorchestrationstatus?view=azure-dotnet) is passed to your templates as a parameter. Mind the property names and their casing.
NOTE2: code inside your templates is still subject to these [Content Security Policies](https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/durablefunctionsmonitor.react/public/index.html#L8), so no external scripts, sorry.
## 12. Connect to different Durable Function Hubs and Azure Storage Accounts:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/manage-connection.png" width="609">
## 13. Monitor non-default Storage Providers (Netherite, Microsoft SQL, etc.):
For that you can use Durable Functions Monitor in 'injected' mode, aka added as a [NuGet package](https://www.nuget.org/profiles/durablefunctionsmonitor) to *your* project.
1. Create a .Net Core Function App project, that is [configured to use an alternative Storage Provider](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-storage-providers#azure-storage) and make sure it compiles and starts.
2. Add [DurableFunctionsMonitor.DotNetBackend](https://www.nuget.org/profiles/durablefunctionsmonitor) package to it:
```
dotnet add package DurableFunctionsMonitor.DotNetBackend
```
4. Add mandatory initialization code, that needs to run at your Function's startup:
```
[assembly: WebJobsStartup(typeof(StartupNs.Startup))]
namespace StartupNs
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
DfmEndpoint.Setup();
}
}
}
```
Find more details on programmatic configuration options in the [package readme](https://www.nuget.org/packages/DurableFunctionsMonitor.DotNetBackend/).
6. Run the project:
```
func start
```
8. Navigate to `http://localhost:7071/api`.
You can customize the endpoint address as needed, as described [here](https://www.nuget.org/packages/DurableFunctionsMonitor.DotNetBackend/).
## 14. Visualize your Azure Function projects in form of an interactive graph:
This functionality is powered by [az-func-as-a-graph](https://github.com/scale-tone/az-func-as-a-graph/blob/main/README.md) tool, but now it is also fully integrated into Durable Functions Monitor:
![image](https://user-images.githubusercontent.com/5447190/127571400-f83c7f96-55bc-4714-8323-04d26f3be74f.png)
When running Durable Functions Monitor as [VsCode Extension](https://marketplace.visualstudio.com/items?itemName=DurableFunctionsMonitor.durablefunctionsmonitor), the **Functions Graph** tab should appear automatically, once you have the relevant Functions project opened.
When running in [standalone/injected mode](https://github.com/scale-tone/DurableFunctionsMonitor/tree/master/durablefunctionsmonitor.dotnetbackend#how-to-run) you'll need to generate and upload an intermediate Functions Map JSON file.
1. Generate it with [az-func-as-a-graph CLI](https://github.com/scale-tone/az-func-as-a-graph/blob/main/README.md#how-to-run-as-part-of-azure-devops-build-pipeline). Specify `dfm-func-map.<my-task-hub-name>.json` (will be applied to that particular Task Hub only) or just `dfm-func-map.json` (will be applied to all Task Hubs) as the output name.
2. Upload this generated JSON file to `function-maps` virtual folder inside `durable-functions-monitor` BLOB container in the underlying Storage Account (the full path should look like `/durable-functions-monitor/function-maps/dfm-func-map.<my-task-hub-name>.json`).
3. Restart Durable Functions Monitor.
4. Observe the newly appeared **Functions Graph** tab.
## Contributing

Просмотреть файл

@ -1,25 +1,11 @@
# TODO: The maintainer of this repo has not yet edited this file
**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
- **No CSS support:** Fill out this template with information about how to file issues and get help.
- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport).
- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide.
*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
# Support
## How to file issues and get help
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
This project uses [GitHub Issues](https://github.com/microsoft/DurableFunctionsMonitor/issues) to track bugs and feature requests. Please search the existing
issues before filing new issues to avoid duplicates. For new issues, file your bug or
feature request as a new Issue.
For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
## Microsoft Support Policy
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.

117
azure-pipelines.yml Normal file
Просмотреть файл

@ -0,0 +1,117 @@
pool:
name: Azure Pipelines
vmImage: 'ubuntu-18.04'
demands: npm
steps:
- task: Npm@1
displayName: 'npm install durablefunctionsmonitor.react'
inputs:
workingDir: durablefunctionsmonitor.react
verbose: false
- task: Npm@1
displayName: 'npm build durablefunctionsmonitor.react'
inputs:
command: custom
workingDir: durablefunctionsmonitor.react
verbose: false
customCommand: 'run build'
- task: CopyFiles@2
displayName: 'copy statics to durablefunctionsmonitor.dotnetbackend/DfmStatics'
inputs:
SourceFolder: durablefunctionsmonitor.react/build
Contents: |
static/**
index.html
favicon.png
logo.svg
service-worker.js
manifest.json
TargetFolder: durablefunctionsmonitor.dotnetbackend/DfmStatics
CleanTargetFolder: true
- task: CopyFiles@2
displayName: 'copy durablefunctionsmonitor.dotnetbackend to ArtifactStagingDirectory'
inputs:
SourceFolder: durablefunctionsmonitor.dotnetbackend
TargetFolder: '$(Build.ArtifactStagingDirectory)/durablefunctionsmonitor.dotnetbackend'
OverWrite: true
- task: DotNetCoreCLI@2
displayName: 'dotnet test tests/durablefunctionsmonitor.dotnetbackend.tests'
inputs:
command: 'test'
projects: 'tests/durablefunctionsmonitor.dotnetbackend.tests/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish durablefunctionsmonitor.dotnetbackend'
inputs:
command: publish
publishWebProjects: false
projects: durablefunctionsmonitor.dotnetbackend
arguments: '--output $(Build.ArtifactStagingDirectory)/output'
zipAfterPublish: false
modifyOutputPath: false
- task: CopyFiles@2
displayName: 'copy dotnetbackend to durablefunctionsmonitor-vscodeext/backend'
inputs:
SourceFolder: '$(Build.ArtifactStagingDirectory)/output'
Contents: |
**
!logo.svg
TargetFolder: 'durablefunctionsmonitor-vscodeext/backend'
CleanTargetFolder: true
- task: CopyFiles@2
displayName: 'copy custom-backends to durablefunctionsmonitor-vscodeext/custom-backends'
inputs:
SourceFolder: 'custom-backends'
Contents: |
**
!*.md
TargetFolder: 'durablefunctionsmonitor-vscodeext/custom-backends'
CleanTargetFolder: true
- task: Npm@1
displayName: 'npm install durablefunctionsmonitor-vscodeext'
inputs:
workingDir: 'durablefunctionsmonitor-vscodeext'
verbose: false
- task: Npm@1
displayName: 'package durablefunctionsmonitor-vscodeext to VSIX-file'
inputs:
command: custom
workingDir: 'durablefunctionsmonitor-vscodeext'
verbose: false
customCommand: 'run package'
- task: CopyFiles@2
displayName: 'copy VSIX-file to ArtifactStagingDirectory'
inputs:
SourceFolder: 'durablefunctionsmonitor-vscodeext'
Contents: 'durablefunctionsmonitor*.vsix'
TargetFolder: '$(Build.ArtifactStagingDirectory)'
OverWrite: true
- task: CopyFiles@2
displayName: 'copy LICENSE to output'
inputs:
Contents: |
LICENSE
TargetFolder: '$(Build.ArtifactStagingDirectory)/output'
OverWrite: true
- task: NuGetCommand@2
displayName: 'package dotnetbackend into a Nuget package'
inputs:
command: 'pack'
packagesToPack: '$(Build.ArtifactStagingDirectory)/output/nuspec.nuspec'
packDestination: '$(Build.ArtifactStagingDirectory)'
versioningScheme: 'off'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'

Просмотреть файл

@ -0,0 +1,7 @@
# Custom backends for Durable Functions Monitor
These are Azure Function projects with Durable Functions Monitor 'injected' as a [NuGet package](https://www.nuget.org/profiles/durablefunctionsmonitor). To be used for e.g. monitoring [custom storage providers](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-storage-providers).
* [netcore21](https://github.com/scale-tone/DurableFunctionsMonitor/tree/master/custom-backends/netcore21) - (legacy) Durable Functions Monitor backend, that runs on .Net Core 2.1.
* [netcore31](https://github.com/scale-tone/DurableFunctionsMonitor/tree/master/custom-backends/netcore31) - Durable Functions Monitor backend, that runs on .Net Core 3.1.
* [mssql](https://github.com/scale-tone/DurableFunctionsMonitor/tree/master/custom-backends/mssql) - Durable Functions Monitor backend to be used with [Durable Task SQL Provider](https://microsoft.github.io/durabletask-mssql/#/).

267
custom-backends/mssql/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,267 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# nuget.exe
nuget.exe
# Azure Functions localsettings file
local.settings.json
# User-specific files
*.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/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.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
# 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
# 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
# TODO: 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
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable 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
# 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
node_modules/
orleans.codegen.cs
# 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
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# 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

Просмотреть файл

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="3.0.1" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.12" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.6.0" />
<PackageReference Include="Microsoft.DurableTask.SqlServer.AzureFunctions" Version="0.10.1-beta" />
<PackageReference Include="durablefunctionsmonitor.dotnetbackend" Version="5.1.1" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,39 @@
# Durable Functions Monitor for MSSQL storage provider
Custom Durable Functions Monitor backend project to be used with [Durable Task SQL Provider](https://microsoft.github.io/durabletask-mssql/#/).
## How to run locally
* Clone this repo.
* In the project's folder create a `local.settings.json` file, which should look like this:
```
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsSecretStorageType": "files",
"DFM_SQL_CONNECTION_STRING": "your-mssql-connection-string",
"DFM_HUB_NAME": "mssql",
"DFM_NONCE": "i_sure_know_what_i_am_doing",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
},
"Host": {
"LocalHttpPort": 7072
}
}
```
* Go to the project's folder with your command prompt and type the following:
```
func start
```
* Navigate to http://localhost:7072
# How to deploy to Azure
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fscale-tone%2FDurableFunctionsMonitor%2Fmaster%2Fcustom-backends%2Fmssql%2Farm-template.json)
The above button will deploy *these sources* into *your newly created* Function App instance.

Просмотреть файл

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using DurableFunctionsMonitor.DotNetBackend;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Data.SqlClient;
[assembly: WebJobsStartup(typeof(Dfm.MsSql.Startup))]
namespace Dfm.MsSql
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
DfmEndpoint.Setup(null, new DfmExtensionPoints { GetInstanceHistoryRoutine = GetInstanceHistory });
}
/// <summary>
/// Custom routine for fetching orchestration history
/// </summary>
public static IEnumerable<HistoryEvent> GetInstanceHistory(IDurableClient durableClient, string connName, string hubName, string instanceId)
{
string sql =
@"SELECT
IIF(h2.TaskID IS NULL, h.Timestamp, h2.Timestamp) as Timestamp,
IIF(h2.TaskID IS NULL, h.EventType, h2.EventType) as EventType,
h.TaskID as EventId,
h.Name as Name,
IIF(h2.TaskID IS NULL, NULL, h.Timestamp) as ScheduledTime,
p.Text as Result,
p.Reason as Details,
cih.InstanceID as SubOrchestrationId
FROM
dt.Instances i
INNER JOIN
dt.History h
ON
(i.InstanceID = h.InstanceID AND i.ExecutionID = h.ExecutionID)
LEFT JOIN
dt.History h2
ON
(
h.EventType IN ('TaskScheduled', 'SubOrchestrationInstanceCreated')
AND
h2.EventType IN ('SubOrchestrationInstanceCompleted', 'SubOrchestrationInstanceFailed', 'TaskCompleted', 'TaskFailed')
AND
h.InstanceID = h2.InstanceID AND h.ExecutionID = h2.ExecutionID AND h.TaskID = h2.TaskID AND h.SequenceNumber != h2.SequenceNumber
)
LEFT JOIN
dt.Payloads p
ON
p.PayloadID = h2.DataPayloadID
LEFT JOIN
(
select
cii.ParentInstanceID,
cii.InstanceID,
chh.TaskID
from
dt.Instances cii
INNER JOIN
dt.History chh
ON
(chh.InstanceID = cii.InstanceID AND chh.EventType = 'ExecutionStarted')
) cih
ON
(cih.ParentInstanceID = h.InstanceID AND cih.TaskID = h.TaskID)
WHERE
h.EventType IN
(
'ExecutionStarted', 'ExecutionCompleted', 'ExecutionFailed', 'ExecutionTerminated', 'TaskScheduled', 'SubOrchestrationInstanceCreated',
'ContinueAsNew', 'TimerCreated', 'TimerFired', 'EventRaised', 'EventSent'
)
AND
i.InstanceID = @OrchestrationInstanceId
ORDER BY
h.SequenceNumber";
string sqlConnectionString = Environment.GetEnvironmentVariable("DFM_SQL_CONNECTION_STRING");
using (var conn = new SqlConnection(sqlConnectionString))
{
conn.Open();
using (var cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@OrchestrationInstanceId", instanceId);
using (SqlDataReader reader = cmd.ExecuteReader())
{
// Memorizing 'ExecutionStarted' event, to further correlate with 'ExecutionCompleted'
DateTimeOffset? executionStartedTimestamp = null;
while (reader.Read())
{
var evt = ToHistoryEvent(reader, executionStartedTimestamp);
if (evt.EventType == "ExecutionStarted")
{
executionStartedTimestamp = evt.Timestamp;
}
yield return evt;
}
}
}
}
}
private static HistoryEvent ToHistoryEvent(SqlDataReader reader, DateTimeOffset? executionStartTime)
{
var evt = new HistoryEvent
{
Timestamp = ((DateTime)reader["Timestamp"]).ToUniversalTime(),
EventType = reader["EventType"].ToString(),
EventId = reader["EventId"] is DBNull ? null : (int?)reader["EventId"],
Name = reader["Name"].ToString(),
Result = reader["Result"].ToString(),
Details = reader["Details"].ToString(),
SubOrchestrationId = reader["SubOrchestrationId"].ToString(),
};
var rawScheduledTime = reader["ScheduledTime"];
if (!(rawScheduledTime is DBNull))
{
evt.ScheduledTime = ((DateTime)rawScheduledTime).ToUniversalTime();
}
else if(evt.EventType == "ExecutionCompleted")
{
evt.ScheduledTime = executionStartTime?.ToUniversalTime();
}
if (evt.ScheduledTime.HasValue)
{
evt.DurationInMs = (evt.Timestamp - evt.ScheduledTime.Value).TotalMilliseconds;
}
return evt;
}
}
}

Просмотреть файл

@ -0,0 +1,155 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"functionAppName": {
"type": "string",
"defaultValue": "[concat('dfm-',uniqueString(resourceGroup().id))]",
"metadata": {
"description": "Name for the Function App, that will host your DFM instance. NOTE: there will be a NEW app created, and it will be different from the one that hosts your Durable Functions."
}
},
"storageConnectionString": {
"type": "securestring",
"metadata": {
"description": "Storage Connection String to the Storage your Durable Functions reside in. Copy it from your Durable Functions App Settings."
}
},
"sqlConnectionString": {
"type": "securestring",
"metadata": {
"description": "Connection String for the database your MSSQL storage provider is using. Copy it from your Durable Functions App Settings."
}
},
"taskHubName": {
"type": "string",
"metadata": {
"description": "Task Hub name to be monitored"
}
},
"aadAppClientId": {
"type": "string",
"metadata": {
"description": "In Azure Portal->Azure Active Directory->App Registrations create a new AAD App. Set its 'Redirect URI' setting to 'https://[your-function-app].azurewebsites.net/.auth/login/aad/callback'. Then on 'Authentication' page enable ID Tokens. Then copy that AAD App's ClientId into here."
}
},
"aadAppTenantId": {
"type": "string",
"defaultValue": "[subscription().tenantId]",
"metadata": {
"description": "Put your AAD TenantId here (you can find it on Azure Portal->Azure Active Directory page), or leave as default to use the current subscription's TenantId."
}
},
"allowedUserNames": {
"type": "string",
"metadata": {
"description": "Comma-separated list of users (emails), that will be allowed to access this DFM instance. Specify at least yourself here."
}
}
},
"resources": [
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2016-09-01",
"name": "[parameters('functionAppName')]",
"location": "[resourceGroup().location]",
"sku": {
"name": "Y1",
"tier": "Dynamic"
},
"properties": {
"name": "[parameters('functionAppName')]",
"computeMode": "Dynamic"
}
},
{
"apiVersion": "2018-11-01",
"type": "Microsoft.Web/sites",
"name": "[parameters('functionAppName')]",
"location": "[resourceGroup().location]",
"kind": "functionapp",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]"
],
"resources": [
{
"apiVersion": "2015-08-01",
"name": "web",
"type": "sourcecontrols",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
],
"properties": {
"RepoUrl": "https://github.com/scale-tone/DurableFunctionsMonitor",
"branch": "master",
"IsManualIntegration": true
}
},
{
"name": "[concat(parameters('functionAppName'), '/authsettings')]",
"apiVersion": "2018-11-01",
"type": "Microsoft.Web/sites/config",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
],
"properties": {
"enabled": true,
"unauthenticatedClientAction": "RedirectToLoginPage",
"tokenStoreEnabled": true,
"defaultProvider": "AzureActiveDirectory",
"clientId": "[parameters('aadAppClientId')]",
"issuer": "[concat('https://login.microsoftonline.com/', parameters('aadAppTenantId'), '/v2.0')]"
}
}
],
"properties": {
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]",
"siteConfig": {
"appSettings": [
{
"name": "DFM_SQL_CONNECTION_STRING",
"value": "[parameters('sqlConnectionString')]"
},
{
"name": "DFM_HUB_NAME",
"value": "[parameters('taskHubName')]"
},
{
"name": "DFM_ALLOWED_USER_NAMES",
"value": "[parameters('allowedUserNames')]"
},
{
"name": "AzureWebJobsStorage",
"value": "[parameters('storageConnectionString')]"
},
{
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
"value": "[parameters('storageConnectionString')]"
},
{
"name": "WEBSITE_CONTENTSHARE",
"value": "[toLower(parameters('functionAppName'))]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~3"
},
{
"name": "FUNCTIONS_WORKER_RUNTIME",
"value": "dotnet"
},
{
"name": "Project",
"value": "custom-backends/mssql"
}
]
}
}
}
]
}

Просмотреть файл

@ -0,0 +1,19 @@
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": ""
},
"durableTask": {
"hubName": "%DFM_HUB_NAME%",
"extendedSessionsEnabled": "true",
"UseGracefulShutdown": "true",
"storageProvider": {
"type": "mssql",
"connectionStringName": "DFM_SQL_CONNECTION_STRING",
"taskEventLockTimeout": "00:02:00"
}
}
}
}

267
custom-backends/netcore21/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,267 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# nuget.exe
nuget.exe
# Azure Functions localsettings file
local.settings.json
# User-specific files
*.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/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.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
# 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
# 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
# TODO: 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
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable 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
# 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
node_modules/
orleans.codegen.cs
# 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
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# 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

Просмотреть файл

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.38" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.5.1" />
<PackageReference Include="durablefunctionsmonitor.dotnetbackend" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,9 @@
# Durable Functions Monitor on .Net Core 2.1
Custom Durable Functions Monitor backend project, configured to run on .Net Core 2.1.
# How to deploy to Azure
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fscale-tone%2FDurableFunctionsMonitor%2Fmaster%2Fcustom-backends%2Fnetcore21%2Farm-template.json)
The above button will deploy *these sources* into *your newly created* Function App instance.

Просмотреть файл

@ -0,0 +1,15 @@
using DurableFunctionsMonitor.DotNetBackend;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
[assembly: WebJobsStartup(typeof(Dfm.NetCore21.Startup))]
namespace Dfm.NetCore21
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
DfmEndpoint.Setup();
}
}
}

Просмотреть файл

@ -0,0 +1,146 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"functionAppName": {
"type": "string",
"defaultValue": "[concat('dfm-',uniqueString(resourceGroup().id))]",
"metadata": {
"description": "Name for the Function App, that will host your DFM instance. NOTE: there will be a NEW app created, and it will be different from the one that hosts your Durable Functions."
}
},
"storageConnectionString": {
"type": "securestring",
"metadata": {
"description": "Storage Connection String to the Storage your Durable Functions reside in. Copy it from your Durable Functions App Settings."
}
},
"taskHubName": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "(optional) Comma-separated list of Task Hub names to be monitored. WARNING: if not set, this instance will expose ALL Task Hubs in your Storage account!"
}
},
"aadAppClientId": {
"type": "string",
"metadata": {
"description": "In Azure Portal->Azure Active Directory->App Registrations create a new AAD App. Set its 'Redirect URI' setting to 'https://[your-function-app].azurewebsites.net/.auth/login/aad/callback'. Then on 'Authentication' page enable ID Tokens. Then copy that AAD App's ClientId into here."
}
},
"aadAppTenantId": {
"type": "string",
"defaultValue": "[subscription().tenantId]",
"metadata": {
"description": "Put your AAD TenantId here (you can find it on Azure Portal->Azure Active Directory page), or leave as default to use the current subscription's TenantId."
}
},
"allowedUserNames": {
"type": "string",
"metadata": {
"description": "Comma-separated list of users (emails), that will be allowed to access this DFM instance. Specify at least yourself here."
}
}
},
"resources": [
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2016-09-01",
"name": "[parameters('functionAppName')]",
"location": "[resourceGroup().location]",
"sku": {
"name": "Y1",
"tier": "Dynamic"
},
"properties": {
"name": "[parameters('functionAppName')]",
"computeMode": "Dynamic"
}
},
{
"apiVersion": "2018-11-01",
"type": "Microsoft.Web/sites",
"name": "[parameters('functionAppName')]",
"location": "[resourceGroup().location]",
"kind": "functionapp",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]"
],
"resources": [
{
"apiVersion": "2015-08-01",
"name": "web",
"type": "sourcecontrols",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
],
"properties": {
"RepoUrl": "https://github.com/scale-tone/DurableFunctionsMonitor",
"branch": "master",
"IsManualIntegration": true
}
},
{
"name": "[concat(parameters('functionAppName'), '/authsettings')]",
"apiVersion": "2018-11-01",
"type": "Microsoft.Web/sites/config",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
],
"properties": {
"enabled": true,
"unauthenticatedClientAction": "RedirectToLoginPage",
"tokenStoreEnabled": true,
"defaultProvider": "AzureActiveDirectory",
"clientId": "[parameters('aadAppClientId')]",
"issuer": "[concat('https://login.microsoftonline.com/', parameters('aadAppTenantId'), '/v2.0')]"
}
}
],
"properties": {
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]",
"siteConfig": {
"appSettings": [
{
"name": "DFM_HUB_NAME",
"value": "[parameters('taskHubName')]"
},
{
"name": "DFM_ALLOWED_USER_NAMES",
"value": "[parameters('allowedUserNames')]"
},
{
"name": "AzureWebJobsStorage",
"value": "[parameters('storageConnectionString')]"
},
{
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
"value": "[parameters('storageConnectionString')]"
},
{
"name": "WEBSITE_CONTENTSHARE",
"value": "[toLower(parameters('functionAppName'))]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~2"
},
{
"name": "FUNCTIONS_WORKER_RUNTIME",
"value": "dotnet"
},
{
"name": "Project",
"value": "custom-backends/netcore21"
}
]
}
}
}
]
}

Просмотреть файл

@ -0,0 +1,8 @@
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": ""
}
}
}

267
custom-backends/netcore31/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,267 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# nuget.exe
nuget.exe
# Azure Functions localsettings file
local.settings.json
# User-specific files
*.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/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.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
# 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
# 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
# TODO: 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
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable 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
# 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
node_modules/
orleans.codegen.cs
# 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
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# 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

Просмотреть файл

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.12" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.5.0" />
<PackageReference Include="durablefunctionsmonitor.dotnetbackend" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,9 @@
# Durable Functions Monitor on .Net Core 3.1
Custom Durable Functions Monitor backend project, configured to run on .Net Core 3.1.
# How to deploy to Azure
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fscale-tone%2FDurableFunctionsMonitor%2Fmaster%2Fcustom-backends%2Fnetcore31%2Farm-template.json)
The above button will deploy *these sources* into *your newly created* Function App instance.

Просмотреть файл

@ -0,0 +1,15 @@
using DurableFunctionsMonitor.DotNetBackend;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
[assembly: WebJobsStartup(typeof(Dfm.NetCore31.Startup))]
namespace Dfm.NetCore31
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
DfmEndpoint.Setup();
}
}
}

Просмотреть файл

@ -0,0 +1,146 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"functionAppName": {
"type": "string",
"defaultValue": "[concat('dfm-',uniqueString(resourceGroup().id))]",
"metadata": {
"description": "Name for the Function App, that will host your DFM instance. NOTE: there will be a NEW app created, and it will be different from the one that hosts your Durable Functions."
}
},
"storageConnectionString": {
"type": "securestring",
"metadata": {
"description": "Storage Connection String to the Storage your Durable Functions reside in. Copy it from your Durable Functions App Settings."
}
},
"taskHubName": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "(optional) Comma-separated list of Task Hub names to be monitored. WARNING: if not set, this instance will expose ALL Task Hubs in your Storage account!"
}
},
"aadAppClientId": {
"type": "string",
"metadata": {
"description": "In Azure Portal->Azure Active Directory->App Registrations create a new AAD App. Set its 'Redirect URI' setting to 'https://[your-function-app].azurewebsites.net/.auth/login/aad/callback'. Then on 'Authentication' page enable ID Tokens. Then copy that AAD App's ClientId into here."
}
},
"aadAppTenantId": {
"type": "string",
"defaultValue": "[subscription().tenantId]",
"metadata": {
"description": "Put your AAD TenantId here (you can find it on Azure Portal->Azure Active Directory page), or leave as default to use the current subscription's TenantId."
}
},
"allowedUserNames": {
"type": "string",
"metadata": {
"description": "Comma-separated list of users (emails), that will be allowed to access this DFM instance. Specify at least yourself here."
}
}
},
"resources": [
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2016-09-01",
"name": "[parameters('functionAppName')]",
"location": "[resourceGroup().location]",
"sku": {
"name": "Y1",
"tier": "Dynamic"
},
"properties": {
"name": "[parameters('functionAppName')]",
"computeMode": "Dynamic"
}
},
{
"apiVersion": "2018-11-01",
"type": "Microsoft.Web/sites",
"name": "[parameters('functionAppName')]",
"location": "[resourceGroup().location]",
"kind": "functionapp",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]"
],
"resources": [
{
"apiVersion": "2015-08-01",
"name": "web",
"type": "sourcecontrols",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
],
"properties": {
"RepoUrl": "https://github.com/scale-tone/DurableFunctionsMonitor",
"branch": "master",
"IsManualIntegration": true
}
},
{
"name": "[concat(parameters('functionAppName'), '/authsettings')]",
"apiVersion": "2018-11-01",
"type": "Microsoft.Web/sites/config",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
],
"properties": {
"enabled": true,
"unauthenticatedClientAction": "RedirectToLoginPage",
"tokenStoreEnabled": true,
"defaultProvider": "AzureActiveDirectory",
"clientId": "[parameters('aadAppClientId')]",
"issuer": "[concat('https://login.microsoftonline.com/', parameters('aadAppTenantId'), '/v2.0')]"
}
}
],
"properties": {
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppName'))]",
"siteConfig": {
"appSettings": [
{
"name": "DFM_HUB_NAME",
"value": "[parameters('taskHubName')]"
},
{
"name": "DFM_ALLOWED_USER_NAMES",
"value": "[parameters('allowedUserNames')]"
},
{
"name": "AzureWebJobsStorage",
"value": "[parameters('storageConnectionString')]"
},
{
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
"value": "[parameters('storageConnectionString')]"
},
{
"name": "WEBSITE_CONTENTSHARE",
"value": "[toLower(parameters('functionAppName'))]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~3"
},
{
"name": "FUNCTIONS_WORKER_RUNTIME",
"value": "dotnet"
},
{
"name": "Project",
"value": "custom-backends/netcore31"
}
]
}
}
}
]
}

Просмотреть файл

@ -0,0 +1,8 @@
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": ""
}
}
}

100
durablefunctionsmonitor-vscodeext/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,100 @@
# build output
*.vsix
# DurableFunctionsMonitor.Functions build output
backend
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TypeScript output
dist
out
# Azure Functions artifacts
bin
obj
appsettings.json
local.settings.json

7
durablefunctionsmonitor-vscodeext/.vscode/extensions.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

41
durablefunctionsmonitor-vscodeext/.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,41 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: watch"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "npm: watch"
},
{
"name": "Local Process with Kubernetes (Preview)",
"type": "dev-spaces-connect-configuration",
"request": "launch"
}
]
}

11
durablefunctionsmonitor-vscodeext/.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,11 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

20
durablefunctionsmonitor-vscodeext/.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,20 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

Просмотреть файл

@ -0,0 +1,10 @@
.vscode/**
.vscode-test/**
out/test/**
src/**
.gitignore
vsc-extension-quickstart.md
**/tsconfig.json
**/tslint.json
**/*.map
**/*.ts

Просмотреть файл

@ -0,0 +1,199 @@
# Change Log
## Version 5.1.0
- Instance execution history can now be filtered by time and other field values:
![image](https://user-images.githubusercontent.com/5447190/140803804-84ef440b-bce7-432d-aaf9-4b663f2ef5cd.png)
- 'In' and 'Not In' filter operators. Filter values should be comma-separated or in form of a JSON array.
- Backend migrated to .Net Core 3.1.
- Direct requests that DfMon makes against Azure Table Storage now contain custom **User-Agent** header: `DurableFunctionsMonitor-Standalone`, `DurableFunctionsMonitor-VsCodeExt` or `DurableFunctionsMonitor-Injected`. Note that the majority of calls is still done via DurableClient, and those cannot be instrumented like this yet.
- Minor bugfixes.
## Version 5.0.0
- UI improvements for instance filter and in some other places.
- Minor bugfixes.
## Version 4.8.2
- Minor hotfix (DfMon's View Container might become unresponsive after a debug session).
## Version 4.8.1
- Workaround for https://github.com/Azure/azure-functions-durable-extension/issues/1926 (being unable to execute .Reset() and .StartNew() against a Task Hub named 'TestHubName').
## Version 4.8
- 'Start New Orchestration Instance' feature:
<img width="200px" src="https://user-images.githubusercontent.com/5447190/130657962-c1c32575-c82c-4e29-ad88-3951eb821fe8.png"/>
<img width="400px" src="https://user-images.githubusercontent.com/5447190/130658737-e51e259d-e7ec-43a2-902b-79907936fb82.png"/>
- Should now work seamlessly in [GitHub Codespaces](https://github.com/features/codespaces).
- Full support for [Microsoft SQL storage provider](https://github.com/microsoft/durabletask-mssql).
- Latest [az-func-as-a-graph](https://github.com/scale-tone/az-func-as-a-graph) integrated.
- Minor bugfixes.
## Version 4.7.1
- Hotfix for incompatibility with Storage Emulator ([#112](https://github.com/scale-tone/DurableFunctionsMonitor/issues/112)).
## Version 4.7
- Latest [az-func-as-a-graph](https://github.com/scale-tone/az-func-as-a-graph) integrated, and it is now used as yet another visualization tab for both search results and instance details, with instance counts and statuses rendered on top of it. So it now acts as an *animated* code map of your project:
![image](https://user-images.githubusercontent.com/5447190/127571400-f83c7f96-55bc-4714-8323-04d26f3be74f.png)
- 'Open XXXInstances/XXXHistory in Storage Explorer' menu items for Task Hubs:
<img src="https://user-images.githubusercontent.com/5447190/127571803-4502d249-9963-4f70-9c4e-8aa1397bf06e.png" width="300">
- Long JSON (or just long error messages) can now be viewed in a popup window ([#109](https://github.com/scale-tone/DurableFunctionsMonitor/issues/109)).
- Minor bugfixes.
## Version 4.6
- Added a sortable **Duration** column to the list of results. Now you can quickly find quickest and longest instances.
- Gantt charts are now interactive (lines are clickable).
- Custom backends: you can now switch to a .Net Core 3.1 backend, or even to your own customized one:
![image](https://user-images.githubusercontent.com/5447190/123545702-c3aeb500-d759-11eb-9d6d-7c69db167ca2.png)
- (Limited) support for [Microsoft SQL storage provider](https://github.com/microsoft/durabletask-mssql). When you open a project that uses it, the relevant Task Hub should appear in the **DURABLE FUNCTIONS** view container:
![image](https://user-images.githubusercontent.com/5447190/123545989-281e4400-d75b-11eb-865e-b8aa3cee690a.png)
- Minor bugfixes.
## Version 4.5
- Time can now be shown in local time zone. **File->Preferences->Settings->Durable Functions Monitor->Show Time As**.
- F# support for Functions Graphs.
- Instance Details tab is now integrated with Functions Graph. If relevant Functions project is currently open, the Details tab will allow navigating to Functions Graph and to Orchestration/Entity/Activity source code.
- Minor bugfixes.
## Version 4.4
- Now you can get a quick overview of _any_ Azure Functions project in form of a graph. **Command Palette -> Visualize Functions as a Graph...**. For Durable Functions/Durable Entities the tool also tries to infer and show their relationships. Function nodes are clickable and lead to function's code.
- Minor bugfixes.
## Version 4.3
- Fixed time ranges ('Last Minute', 'Last Hour' etc.).
- Multiple choice for filtering by instance status ('Running', 'Completed' etc.).
- 'Not Equals', 'Not Starts With' and 'Not Contains' filter operators.
- Performance improvements.
- Minor bugfixes.
## Version 4.2
- Orchestrations/Entities are now also visualized as a time histogram and as a Gantt chart. Time histogram is interactive, you can zoom it in/out with your mouse.
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/time-histogram.png" width="400">
- 'Send Signal' button for Durable Entities.
- Minor bugfixes.
## Version 4.1
- Dark color mode.
- Minor bugfixes.
## Version 4.0
- It is now one backend per Storage Account, not per each Task Hub. Works faster and consumes less resources.
- Minor bugfixes.
## Version 3.9
- Gantt Charts for orchestrations (in addition to Sequence Diagrams).
- 'Go to instanceId...' feature to quickly navigate to an orchestration/entity instance by its id (with autocomplete supported). **Right-click on a Task Hub->Go to instanceId...**.
- DotLiquid replaced with [Fluid](https://github.com/sebastienros/fluid) for rendering custom status tabs. [Fluid](https://github.com/sebastienros/fluid) looks much more mature (most of [Liquid](https://shopify.github.io/liquid/) seems to be supported) and more alive library.
- 'Save as .SVG' button for diagrams.
- Status tabs now refresh much smoother.
- Minor bugfixes.
## Version 3.8
- WebViews are now persistent (do not reload every time you switch between them) and even persist their state (filters, sorting etc.) across restarts.
- 'Restart' button for orchestrations (triggers the new [.RestartAsync()](https://github.com/Azure/azure-functions-durable-extension/pull/1545) method).
- Sequence diagrams now show some timing (start times and durations).
- 'Detach from all Task Hubs...' button for quickly killing all backends.
- All logs (when enabled) now go to 'Durable Functions Monitor' output channel.
- Minor bugfixes.
## Version 3.7
- Now settings are stored in VsCode's settings.json. **File->Preferences->Settings->Durable Functions Monitor**:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-settings.png" width="400">
- Local Storage Emulator, Azure Government and other exotic Storage Account types are now supported. If your Local Storage Emulator is running and there're some TaskHubs in it - they will appear automatically on your Azure Functions View Container (if not, try to modify the 'Storage Emulator Connection String' parameter on the Settings page).
- Long-awaited 'Cancel' button on the Orchestrations page.
- Now you can hide the columns you're not interested in:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/hide-columns.png" width="350">
- Minor other UI improvements.
## Version 3.6
- 'Clear Entity Storage...' menu item for doing garbage collection of deleted Durable Entities. Executes the recently added [IDurableEntityClient.CleanEntityStorageAsync()](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.idurableentityclient.cleanentitystorageasync?view=azure-dotnet) method.
- Custom status visualisation for orchestrations/entities in form of [Liquid templates](https://shopify.github.io/liquid/).
1. Create a [DotLiquid](https://github.com/dotliquid/dotliquid) template file.
2. Name it like `[My Custom Tab Name].[orchestration-or-entity-name].liquid` or just `[My Custom Tab Name].liquid` (this one will be applied to any kind of entity).
3. In the same Storage Account create a container called `durable-functions-monitor`.
4. Put your template file into a `tab-templates` virtual folder in that container (the full path should look like `/durable-functions-monitor/tab-templates/[My Custom Tab Name].[orchestration-or-entity-name].liquid`).
5. Restart Durable Functions Monitor.
6. Observe the newly appeared `My Custom Tab Name` tab on the Orchestration/Entity Details page.
- Performance improvements for loading the list of Orchestrations/Entities.
## Version 3.5
- Now the **Orchestration Details** page features a nice [mermaid](https://www.npmjs.com/package/mermaid)-based sequence diagram:
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestration-diagram-small.png">
- Also it's now possible to navigate to suborchestrations from the history list on the **Orchestration Details** page.
## Version 3.4
- Now integrated with [Azure Account](https://marketplace.visualstudio.com/items?itemName=ms-vscode.azure-account) extension, so once logged in to Azure, you can now see and connect to all your TaskHubs. It is also still possible to connect with connection strings, as before. NOTE1: only filtered Azure Subscriptions are shown, so make sure your filter is set correctly with [Azure: Select Subscriptions](https://docs.microsoft.com/en-us/azure/governance/policy/how-to/extension-for-vscode#select-subscriptions) command. NOTE2: many things can go wrong when fetching the list of TaskHubs, so to investigate those problems you can [enable logging](https://github.com/scale-tone/DurableFunctionsMonitor/blob/master/durablefunctionsmonitor-vscodeext/CHANGELOG.md#version-21) and then check the 'Durable Functions Monitor' output channel.
## Version 3.3
- customStatus value of your orchestration instances can now be changed with 'Set Custom Status' button.
- Minor bugfixes.
## Version 3.2
- You can now delete unused Task Hubs with 'Delete Task Hub...' context menu item.
- Better (non-native) DateTime pickers.
## Version 3.1
- Minor security improvements.
- List of existing Task Hubs is now loaded from your Storage Account and shown to you, when connecting to a Task Hub.
## Version 3.0
- A 'DURABLE FUNCTIONS' TreeView added to Azure Functions View Container. It displays all currently attached Task Hubs, allows to connect to multiple Task Hubs and switch between them. You need to have [Azure Functions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) extension installed to see it (which is typically the case if you work with Azure Functions in VSCode).
## Version 2.2
- Bulk purge for Durable Entities as well.
- Prettified JSON on instance details page.
## Version 2.1
- Instances list sort order is now persisted as well.
- Whenever backend initialization fails, its error message is now being shown immediately (instead of a generic 'timeout' message as before).
- A complete backend output can now be logged into a file for debugging purposes. Open the **settings.json** file in extension's folder and set the **logging** setting to **true**. That will produce a **backend/backend-37072.log** file with full console output from func.exe.
## Version 2.0
- More native support for Durable Entities.
- Backend migrated to Microsoft.Azure.WebJobs.Extensions.DurableTask 2.0.0. Please, ensure you have the latest Azure Functions Core Tools installed globally, otherwise the backend might fail to start.
- Now displaying connection info (storage account name/hub name) in the tab title.
## Version 1.3
- Implemented purging orchestration instance history. Type 'Purge Durable Functions History...' in your Command Palette.
- Added a context menu over a **host.json** file.

Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 scale-tone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Просмотреть файл

@ -0,0 +1,37 @@
# Durable Functions Monitor as a VsCode Extension
List/monitor/debug your Azure Durable Functions inside VsCode.
**Command Palette -> Durable Functions Monitor**, or (if you have [Azure Functions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) extension also installed) **Azure Functions View Container -> DURABLE FUNCTIONS**, or right-click on your **host.json** file and use the context menu.
## Features
* Get a bird's eye view of any Azure Functions project in form of a graph - **Command Palette -> Visualize Functions as a Graph...**.
* List your Orchestrations and/or Durable Entities, with sorting, infinite scrolling and auto-refresh.
* Monitor the status of a certain Orchestration/Durable Entity. Restart, Purge, Rewind, Terminate, Raise Events.
* Start new orchestration instances - **Azure Functions View Container -> DURABLE FUNCTIONS -> [right-click on your TaskHub] -> Start New Orchestration Instance...**
* Quickly navigate to an Orchestration/Entity instance by its ID - **Command Palette -> Durable Functions Monitor: Go to instanceId...** or **Azure Functions View Container -> DURABLE FUNCTIONS -> [right-click on your TaskHub] -> Go to instanceId...**
* Purge Orchestrations/Durable Entities history - **Command Palette -> Durable Functions Monitor: Purge History...**
* Cleanup deleted Durable Entities - **Command Palette -> Durable Functions Monitor: Clean Entity Storage...**
* Observe all Task Hubs in your Azure Subscription and connect to them - **Azure Functions View Container -> DURABLE FUNCTIONS**
* Delete Task Hubs - **Command Palette -> Delete Task Hub...**
## Pictures
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-command-palette.png" width="624">
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestrations.png" width="768">
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestration.png" width="843">
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/gantt-chart.png" width="843">
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/vscodeext-orchestration-diagram.png" width="600">
<img src="https://raw.githubusercontent.com/scale-tone/DurableFunctionsMonitor/master/readme/screenshots/function-graph.png" width="800">
## Prerequisites
Make sure you have the latest [Azure Functions Core Tools](https://www.npmjs.com/package/azure-functions-core-tools) globally installed on your devbox.
More info and sources on [the github repo](https://github.com/scale-tone/DurableFunctionsMonitor#features).

Двоичные данные
durablefunctionsmonitor-vscodeext/logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.0 KiB

1866
durablefunctionsmonitor-vscodeext/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,324 @@
{
"name": "durablefunctionsmonitor",
"displayName": "Durable Functions Monitor",
"description": "Monitoring/debugging UI tool for Azure Durable Functions. View->Command Palette...->Durable Functions Monitor",
"version": "5.1.0",
"engines": {
"vscode": "^1.39.0"
},
"categories": [
"Other",
"Debuggers"
],
"homepage": "https://github.com/scale-tone/DurableFunctionsMonitor",
"repository": {
"type": "git",
"url": "https://github.com/scale-tone/DurableFunctionsMonitor"
},
"bugs": {
"url": "https://github.com/scale-tone/DurableFunctionsMonitor/issues"
},
"icon": "logo.png",
"keywords": [
"Azure Durable Functions",
"Azure Durable Entities",
"Azure Functions",
"Serverless",
"Azure"
],
"publisher": "DurableFunctionsMonitor",
"license": "MIT",
"activationEvents": [
"onView:durableFunctionsMonitorTreeView",
"onCommand:extension.durableFunctionsMonitor",
"onCommand:extension.durableFunctionsMonitorPurgeHistory",
"onCommand:extension.durableFunctionsMonitorCleanEntityStorage",
"onCommand:durableFunctionsMonitorTreeView.attachToAnotherTaskHub",
"onCommand:extension.durableFunctionsMonitorGotoInstanceId",
"onCommand:extension.durableFunctionsMonitorVisualizeAsGraph",
"onCommand:durableFunctionsMonitorTreeView.startNewInstance",
"onDebug"
],
"main": "./out/extension.js",
"contributes": {
"views": {
"azure": [
{
"id": "durableFunctionsMonitorTreeView",
"name": "Durable Functions"
}
]
},
"commands": [
{
"command": "extension.durableFunctionsMonitor",
"title": "Durable Functions Monitor"
},
{
"command": "extension.durableFunctionsMonitorPurgeHistory",
"title": "Durable Functions Monitor: Purge History..."
},
{
"command": "extension.durableFunctionsMonitorCleanEntityStorage",
"title": "Durable Functions Monitor: Clean Entity Storage..."
},
{
"command": "extension.durableFunctionsMonitorGotoInstanceId",
"title": "Durable Functions Monitor: Go to instanceId..."
},
{
"command": "extension.durableFunctionsMonitorVisualizeAsGraph",
"title": "Visualize Functions as a Graph..."
},
{
"command": "durableFunctionsMonitorTreeView.attachToTaskHub",
"title": "Attach"
},
{
"command": "durableFunctionsMonitorTreeView.detachFromTaskHub",
"title": "Detach"
},
{
"command": "durableFunctionsMonitorTreeView.openInstancesInStorageExplorer",
"title": "Open *Instances table in Storage Explorer"
},
{
"command": "durableFunctionsMonitorTreeView.openHistoryInStorageExplorer",
"title": "Open *History table in Storage Explorer"
},
{
"command": "durableFunctionsMonitorTreeView.deleteTaskHub",
"title": "Delete Task Hub..."
},
{
"command": "durableFunctionsMonitorTreeView.refresh",
"title": "Refresh",
"icon": {
"light": "resources/light/refresh.svg",
"dark": "resources/dark/refresh.svg"
}
},
{
"command": "durableFunctionsMonitorTreeView.attachToAnotherTaskHub",
"title": "Attach to Task Hub...",
"icon": {
"light": "resources/light/plug.svg",
"dark": "resources/dark/plug.svg"
}
},
{
"command": "durableFunctionsMonitorTreeView.detachFromAllTaskHubs",
"title": "Detach from all Task Hubs...",
"icon": {
"light": "resources/light/unplug.svg",
"dark": "resources/dark/unplug.svg"
}
},
{
"command": "durableFunctionsMonitorTreeView.purgeHistory",
"title": "Purge History..."
},
{
"command": "durableFunctionsMonitorTreeView.cleanEntityStorage",
"title": "Clean Entity Storage..."
},
{
"command": "durableFunctionsMonitorTreeView.gotoInstanceId",
"title": "Go to instanceId..."
},
{
"command": "durableFunctionsMonitorTreeView.startNewInstance",
"title": "Start New Orchestration Instance..."
}
],
"menus": {
"explorer/context": [
{
"command": "extension.durableFunctionsMonitor",
"when": "resourceFilename == host.json",
"group": "DurableFunctionMonitorGroup@1"
},
{
"command": "extension.durableFunctionsMonitorPurgeHistory",
"when": "resourceFilename == host.json",
"group": "DurableFunctionMonitorGroup@2"
},
{
"command": "extension.durableFunctionsMonitorCleanEntityStorage",
"when": "resourceFilename == host.json",
"group": "DurableFunctionMonitorGroup@3"
},
{
"command": "extension.durableFunctionsMonitorGotoInstanceId",
"when": "resourceFilename == host.json",
"group": "DurableFunctionMonitorGroup@4"
},
{
"command": "extension.durableFunctionsMonitorVisualizeAsGraph",
"when": "resourceFilename == host.json",
"group": "DurableFunctionMonitorGroup@5"
}
],
"view/title": [
{
"command": "durableFunctionsMonitorTreeView.refresh",
"when": "view == durableFunctionsMonitorTreeView",
"group": "navigation@1"
},
{
"command": "durableFunctionsMonitorTreeView.detachFromAllTaskHubs",
"when": "view == durableFunctionsMonitorTreeView",
"group": "navigation@2"
},
{
"command": "durableFunctionsMonitorTreeView.attachToAnotherTaskHub",
"when": "view == durableFunctionsMonitorTreeView",
"group": "navigation@3"
}
],
"view/item/context": [
{
"command": "durableFunctionsMonitorTreeView.gotoInstanceId",
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
"group": "2_purge_history@3"
},
{
"command": "durableFunctionsMonitorTreeView.cleanEntityStorage",
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
"group": "2_purge_history@2"
},
{
"command": "durableFunctionsMonitorTreeView.purgeHistory",
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
"group": "2_purge_history@1"
},
{
"command": "durableFunctionsMonitorTreeView.startNewInstance",
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
"group": "2_purge_history@0"
},
{
"command": "durableFunctionsMonitorTreeView.deleteTaskHub",
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached",
"group": "3_delete_task_hub@1"
},
{
"command": "durableFunctionsMonitorTreeView.attachToTaskHub",
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-detached",
"group": "1_attach_detach@1"
},
{
"command": "durableFunctionsMonitorTreeView.detachFromTaskHub",
"when": "view == durableFunctionsMonitorTreeView && viewItem == storageAccount-attached"
},
{
"command": "durableFunctionsMonitorTreeView.openInstancesInStorageExplorer",
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached || viewItem == taskHub-detached",
"group": "4_storage_explorer@1"
},
{
"command": "durableFunctionsMonitorTreeView.openHistoryInStorageExplorer",
"when": "view == durableFunctionsMonitorTreeView && viewItem == taskHub-attached || viewItem == taskHub-detached",
"group": "4_storage_explorer@2"
}
],
"commandPalette": [
{
"command": "durableFunctionsMonitorTreeView.openInstancesInStorageExplorer",
"when": "never"
},
{
"command": "durableFunctionsMonitorTreeView.openHistoryInStorageExplorer",
"when": "never"
}
]
},
"configuration": {
"title": "Durable Functions Monitor",
"properties": {
"durableFunctionsMonitor.backendBaseUrl": {
"type": "string",
"default": "http://localhost:{portNr}/a/p/i",
"description": "URL the backend(s) to be started on. You might want e.g. to change 'localhost' to '127.0.0.1', if you're observing firewall issues. Also it is possible to lock the port number here, if needed (by default it is automatically chosen from the range 37072-38000)."
},
"durableFunctionsMonitor.backendVersionToUse": {
"type": "string",
"enum": [
"Default",
".Net Core 3.1",
".Net Core 2.1"
],
"default": "Default",
"description": "Choose which backend binaries to use when starting a backend. Currently 'Default' backend targets .Net Core 2.1, but you can try other ones, if 'Default' doesn't work for you."
},
"durableFunctionsMonitor.customPathToBackendBinaries": {
"type": "string",
"description": "Put local path to a custom backend implementation to use. Overrides 'Backend Version to Use' when set."
},
"durableFunctionsMonitor.backendTimeoutInSeconds": {
"type": "number",
"default": "60",
"description": "Number of seconds to wait for the backend to start."
},
"durableFunctionsMonitor.storageEmulatorConnectionString": {
"type": "string",
"default": "AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;",
"description": "Connection String to talk to local Storage Emulator. The AccountKey here is a well-known AccountKey. Customize endpoint URLs when needed."
},
"durableFunctionsMonitor.enableLogging": {
"type": "boolean",
"default": false,
"description": "Enable extensive logging and output logs into 'Durable Functions Monitor' output channel"
},
"durableFunctionsMonitor.showTimeAs": {
"type": "string",
"default": "UTC",
"enum": [
"UTC",
"Local"
],
"description": "In which time zone time values should be displayed"
},
"durableFunctionsMonitor.showWhenDebugSessionStarts": {
"type": "boolean",
"default": false,
"description": "Show Durable Functions Monitor when you start debugging a Durable Functions project"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile",
"test": "node ./out/test/runTest.js",
"package": "node ./node_modules/vsce/out/vsce package"
},
"devDependencies": {
"@types/glob": "^7.1.1",
"@types/mocha": "^5.2.6",
"@types/node": "^10.12.21",
"@types/vscode": "^1.39.0",
"glob": "^7.1.4",
"mocha": "^8.2.0",
"tslint": "^5.12.1",
"typescript": "^3.3.1",
"vsce": "^1.88.0",
"vscode-test": "^1.2.0"
},
"dependencies": {
"@azure/arm-storage": "^15.1.0",
"@types/crypto-js": "^3.1.47",
"@types/rimraf": "^3.0.0",
"rimraf": "^3.0.2",
"axios": "^0.21.2",
"crypto-js": "^4.0.0",
"portscanner": "^2.2.0",
"tree-kill": "^1.2.2"
},
"extensionDependencies": [
"ms-vscode.azure-account"
]
}

Просмотреть файл

@ -0,0 +1,16 @@
<svg id="bc18bade-5481-447e-a959-659d72346474"
xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<defs>
<radialGradient id="a445c717-9d75-44c7-ba6b-0d8f2383e560" cx="-36.63" cy="17.12" r="11.18" gradientTransform="translate(41.88 -7.4) scale(0.94 0.94)" gradientUnits="userSpaceOnUse">
<stop offset="0.27" stop-color="#ffd70f"/>
<stop offset="0.49" stop-color="#ffcb12"/>
<stop offset="0.88" stop-color="#feac19"/>
<stop offset="1" stop-color="#fea11b"/>
</radialGradient>
</defs>
<title>Icon-general-2</title>
<path id="e3d1e58c-f78e-4fb5-9857-0c9331da9979" d="M13.56,7.19a2.07,2.07,0,0,0,0-2.93h0L10,.69a2.06,2.06,0,0,0-2.92,0h0L3.52,4.26a2.09,2.09,0,0,0,0,2.93l3,3a.61.61,0,0,1,.17.41v5.52a.7.7,0,0,0,.2.5l1.35,1.35a.45.45,0,0,0,.66,0l1.31-1.31h0l.77-.77a.26.26,0,0,0,0-.38l-.55-.56a.29.29,0,0,1,0-.42l.55-.56a.26.26,0,0,0,0-.38L10.4,13a.28.28,0,0,1,0-.41L11,12a.26.26,0,0,0,0-.38l-.77-.78v-.28Zm-5-5.64A1.18,1.18,0,1,1,7.37,2.73,1.17,1.17,0,0,1,8.54,1.55Z" fill="url(#a445c717-9d75-44c7-ba6b-0d8f2383e560)"/>
<path id="a21a8f7a-61cc-4035-8449-e5c8fe4d4d5e" d="M7.62,16.21h0A.25.25,0,0,0,8,16V11.55a.27.27,0,0,0-.11-.22h0a.25.25,0,0,0-.39.22V16A.27.27,0,0,0,7.62,16.21Z" fill="#ff9300" opacity="0.75"/>
<rect id="ecd3189c-fb1e-4a0e-a2b6-ba2f11dda484" x="5.69" y="5.45" width="5.86" height="0.69" rx="0.32" fill="#ff9300" opacity="0.75"/>
<rect id="a1949a3c-4818-4bd1-b236-0d970b92fc62" x="5.69" y="6.57" width="5.86" height="0.69" rx="0.32" fill="#ff9300" opacity="0.75"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.6 KiB

Просмотреть файл

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="plug.svg"
id="svg4"
version="1.1"
viewBox="0 0 14 16"
height="16"
width="14">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
inkscape:current-layer="svg4"
inkscape:window-maximized="1"
inkscape:window-y="-11"
inkscape:window-x="85"
inkscape:cy="8"
inkscape:cx="7"
inkscape:zoom="45.9375"
showgrid="false"
id="namedview6"
inkscape:window-height="1470"
inkscape:window-width="2160"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<path
style="fill:#c8c8c8;fill-opacity:1"
id="path2"
d="M14 6V5h-4V3H8v1H6c-1.03 0-1.77.81-2 2L3 7c-1.66 0-3 1.34-3 3v2h1v-2c0-1.11.89-2 2-2l1 1c.25 1.16.98 2 2 2h2v1h2v-2h4V9h-4V6h4z"
fill-rule="evenodd" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.6 KiB

Просмотреть файл

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.56253 2.5158C3.46348 3.45013 2 5.55417 2 8.00002C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8.00002C14 5.32522 12.2497 3.05922 9.83199 2.28485L9.52968 3.23835C11.5429 3.88457 13 5.77213 13 8.00002C13 10.7614 10.7614 13 8 13C5.23858 13 3 10.7614 3 8.00002C3 6.31107 3.83742 4.8177 5.11969 3.91248L5.56253 2.5158Z" fill="#C5C5C5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3H2V2H5.5L6 2.5V6H5V3Z" fill="#C5C5C5"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 586 B

Просмотреть файл

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="14"
height="16"
viewBox="0 0 14 16"
version="1.1"
id="svg4"
sodipodi:docname="unplug.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
inkscape:document-rotation="0"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2160"
inkscape:window-height="1470"
id="namedview6"
showgrid="false"
inkscape:zoom="45.9375"
inkscape:cx="7"
inkscape:cy="8"
inkscape:window-x="85"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
fill-rule="evenodd"
d="M14 6V5h-4V3H8v1H6c-1.03 0-1.77.81-2 2L3 7c-1.66 0-3 1.34-3 3v2h1v-2c0-1.11.89-2 2-2l1 1c.25 1.16.98 2 2 2h2v1h2v-2h4V9h-4V6h4z"
id="path2"
style="opacity:0.3;fill:#c8c8c8;fill-opacity:1" />
<g
style="opacity:1;fill:#c8c8c8;fill-opacity:1"
transform="matrix(0.02347216,0,0,0.02347525,-1.8775739,4.5933482)"
id="XMLID_1_">
<path
style="fill:#c8c8c8;fill-opacity:1"
d="m 387,-131 c -141,0 -256,115 -256,256 0,141 115,256 256,256 141,0 256,-115 256,-256 0,-141 -115,-256 -256,-256 z m 0,47.3 c 47.3,0 91.4,15.8 126.8,42.5 l -292.2,293 C 194.8,216.4 178.3,172.3 178.3,125 178.3,10 272,-83.7 387,-83.7 Z m 0,417.4 c -47.3,0 -91.4,-15.8 -126.8,-42.5 L 552.4,-1.8 C 579.2,33.6 595,77.7 595,125 595.7,240 502,333.7 387,333.7 Z"
id="XMLID_6_" />
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.3 KiB

Просмотреть файл

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="plug.svg"
id="svg4"
version="1.1"
viewBox="0 0 14 16"
height="16"
width="14">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
inkscape:current-layer="svg4"
inkscape:window-maximized="1"
inkscape:window-y="-11"
inkscape:window-x="85"
inkscape:cy="8"
inkscape:cx="7"
inkscape:zoom="45.9375"
showgrid="false"
id="namedview6"
inkscape:window-height="1470"
inkscape:window-width="2160"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<path
style="opacity:0.8"
id="path2"
d="M14 6V5h-4V3H8v1H6c-1.03 0-1.77.81-2 2L3 7c-1.66 0-3 1.34-3 3v2h1v-2c0-1.11.89-2 2-2l1 1c.25 1.16.98 2 2 2h2v1h2v-2h4V9h-4V6h4z"
fill-rule="evenodd" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.6 KiB

Просмотреть файл

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.56253 2.5158C3.46348 3.45013 2 5.55417 2 8.00002C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8.00002C14 5.32522 12.2497 3.05922 9.83199 2.28485L9.52968 3.23835C11.5429 3.88457 13 5.77213 13 8.00002C13 10.7614 10.7614 13 8 13C5.23858 13 3 10.7614 3 8.00002C3 6.31107 3.83742 4.8177 5.11969 3.91248L5.56253 2.5158Z" fill="#424242"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3H2V2H5.5L6 2.5V6H5V3Z" fill="#424242"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 586 B

Просмотреть файл

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="unplug.svg"
id="svg4"
version="1.1"
viewBox="0 0 14 16"
height="16"
width="14">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
inkscape:current-layer="svg4"
inkscape:window-maximized="1"
inkscape:window-y="-11"
inkscape:window-x="85"
inkscape:cy="8"
inkscape:cx="7"
inkscape:zoom="45.9375"
showgrid="false"
id="namedview6"
inkscape:window-height="1470"
inkscape:window-width="2160"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:document-rotation="0" />
<path
style="opacity:0.3"
id="path2"
d="M14 6V5h-4V3H8v1H6c-1.03 0-1.77.81-2 2L3 7c-1.66 0-3 1.34-3 3v2h1v-2c0-1.11.89-2 2-2l1 1c.25 1.16.98 2 2 2h2v1h2v-2h4V9h-4V6h4z"
fill-rule="evenodd" />
<g
id="XMLID_1_"
transform="matrix(0.02347216,0,0,0.02347525,-1.8775739,4.5933482)"
style="opacity:0.7">
<path
id="XMLID_6_"
d="m 387,-131 c -141,0 -256,115 -256,256 0,141 115,256 256,256 141,0 256,-115 256,-256 0,-141 -115,-256 -256,-256 z m 0,47.3 c 47.3,0 91.4,15.8 126.8,42.5 l -292.2,293 C 194.8,216.4 178.3,172.3 178.3,125 178.3,10 272,-83.7 387,-83.7 Z m 0,417.4 c -47.3,0 -91.4,-15.8 -126.8,-42.5 L 552.4,-1.8 C 579.2,33.6 595,77.7 595,125 595.7,240 502,333.7 387,333.7 Z" />
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.2 KiB

Просмотреть файл

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="50px" height="50px" viewBox="-0.5 0.5 50 50" enable-background="new -0.5 0.5 50 50" xml:space="preserve">
<path fill="#0072C6" d="M5.757,7.288v36.111c0,3.749,8.392,6.789,18.743,6.789v-42.9C24.5,7.288,5.757,7.288,5.757,7.288z"/>
<path fill="#0072C6" d="M24.243,50.187H24.5c10.351,0,18.743-3.038,18.743-6.788V7.288h-19V50.187z"/>
<path opacity="0.15" fill="#FFFFFF" d="M24.243,50.187H24.5c10.351,0,18.743-3.038,18.743-6.788V7.288h-19V50.187z"/>
<path fill="#FFFFFF" d="M43.243,7.288c0,3.749-8.392,6.788-18.743,6.788S5.757,11.037,5.757,7.288S14.149,0.5,24.5,0.5
S43.243,3.539,43.243,7.288"/>
<path fill="#7FBA00" d="M39.411,6.897c0,2.475-6.676,4.479-14.911,4.479S9.588,9.372,9.588,6.897c0-2.474,6.677-4.479,14.912-4.479
S39.411,4.423,39.411,6.897"/>
<path fill="#B8D432" d="M36.287,9.634c1.952-0.757,3.125-1.705,3.125-2.735c0-2.475-6.676-4.48-14.912-4.48
c-8.235,0-14.911,2.005-14.911,4.48c0,1.03,1.173,1.978,3.125,2.735C15.44,8.576,19.7,7.893,24.5,7.893
C29.301,7.893,33.559,8.576,36.287,9.634"/>
<path fill="#FFFFFF" d="M18.547,32.354c0,1.122-0.407,1.991-1.221,2.607c-0.814,0.616-1.938,0.924-3.373,0.924
c-1.221,0-2.241-0.22-3.061-0.66v-2.64c0.946,0.803,1.988,1.205,3.126,1.205c0.55,0,0.975-0.11,1.275-0.33s0.45-0.511,0.45-0.875
c0-0.357-0.144-0.668-0.433-0.932s-0.876-0.605-1.761-1.023c-1.804-0.846-2.706-2.002-2.706-3.464c0-1.061,0.393-1.912,1.18-2.553
c0.786-0.64,1.831-0.961,3.134-0.961c1.155,0,2.111,0.152,2.871,0.454v2.466c-0.797-0.55-1.705-0.825-2.722-0.825
c-0.511,0-0.915,0.108-1.212,0.325c-0.297,0.218-0.445,0.508-0.445,0.87c0,0.374,0.119,0.681,0.359,0.92
c0.239,0.239,0.73,0.535,1.472,0.887c1.106,0.523,1.893,1.053,2.364,1.592C18.312,30.881,18.547,31.552,18.547,32.354z"/>
<path fill="#FFFFFF" d="M31.274,29.682c0,1.391-0.317,2.599-0.949,3.621c-0.633,1.023-1.523,1.74-2.672,2.153l3.431,3.176H27.62
l-2.45-2.747c-1.05-0.038-1.998-0.316-2.842-0.833c-0.844-0.516-1.496-1.225-1.955-2.124s-0.689-1.902-0.689-3.007
c0-1.226,0.249-2.319,0.746-3.279c0.498-0.96,1.197-1.698,2.099-2.215c0.902-0.516,1.935-0.775,3.102-0.775
c1.088,0,2.063,0.25,2.924,0.751c0.86,0.5,1.528,1.212,2.004,2.136C31.036,27.463,31.274,28.511,31.274,29.682z M28.47,29.831
c0-1.199-0.261-2.146-0.784-2.842s-1.237-1.044-2.145-1.044c-0.924,0-1.663,0.349-2.219,1.047c-0.555,0.699-0.833,1.628-0.833,2.788
c0,1.155,0.272,2.077,0.816,2.767c0.545,0.69,1.267,1.035,2.169,1.035c0.919,0,1.647-0.334,2.186-1.002
C28.2,31.913,28.47,30.996,28.47,29.831z"/>
<polygon fill="#FFFFFF" points="40.273,35.679 33.229,35.679 33.229,23.851 35.893,23.851 35.893,33.518 40.273,33.518 "/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.9 KiB

Просмотреть файл

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="50px"
height="50px"
viewBox="-0.5 0.5 50 50"
enable-background="new -0.5 0.5 50 50"
xml:space="preserve"
sodipodi:docname="mssqlAttached.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)"><metadata
id="metadata25"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs23" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1437"
inkscape:window-height="978"
id="namedview21"
showgrid="false"
inkscape:zoom="14.7"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="56"
inkscape:window-y="-6"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<path
fill="#0072C6"
d="M5.757,7.288v36.111c0,3.749,8.392,6.789,18.743,6.789v-42.9C24.5,7.288,5.757,7.288,5.757,7.288z"
id="path2" />
<path
fill="#0072C6"
d="M24.243,50.187H24.5c10.351,0,18.743-3.038,18.743-6.788V7.288h-19V50.187z"
id="path4" />
<path
opacity="0.15"
fill="#FFFFFF"
d="M24.243,50.187H24.5c10.351,0,18.743-3.038,18.743-6.788V7.288h-19V50.187z"
id="path6" />
<path
fill="#FFFFFF"
d="M43.243,7.288c0,3.749-8.392,6.788-18.743,6.788S5.757,11.037,5.757,7.288S14.149,0.5,24.5,0.5 S43.243,3.539,43.243,7.288"
id="path8" />
<path
fill="#7FBA00"
d="M39.411,6.897c0,2.475-6.676,4.479-14.911,4.479S9.588,9.372,9.588,6.897c0-2.474,6.677-4.479,14.912-4.479 S39.411,4.423,39.411,6.897"
id="path10" />
<path
fill="#B8D432"
d="M36.287,9.634c1.952-0.757,3.125-1.705,3.125-2.735c0-2.475-6.676-4.48-14.912-4.48 c-8.235,0-14.911,2.005-14.911,4.48c0,1.03,1.173,1.978,3.125,2.735C15.44,8.576,19.7,7.893,24.5,7.893 C29.301,7.893,33.559,8.576,36.287,9.634"
id="path12" />
<path
fill="#FFFFFF"
d="M18.547,32.354c0,1.122-0.407,1.991-1.221,2.607c-0.814,0.616-1.938,0.924-3.373,0.924 c-1.221,0-2.241-0.22-3.061-0.66v-2.64c0.946,0.803,1.988,1.205,3.126,1.205c0.55,0,0.975-0.11,1.275-0.33s0.45-0.511,0.45-0.875 c0-0.357-0.144-0.668-0.433-0.932s-0.876-0.605-1.761-1.023c-1.804-0.846-2.706-2.002-2.706-3.464c0-1.061,0.393-1.912,1.18-2.553 c0.786-0.64,1.831-0.961,3.134-0.961c1.155,0,2.111,0.152,2.871,0.454v2.466c-0.797-0.55-1.705-0.825-2.722-0.825 c-0.511,0-0.915,0.108-1.212,0.325c-0.297,0.218-0.445,0.508-0.445,0.87c0,0.374,0.119,0.681,0.359,0.92 c0.239,0.239,0.73,0.535,1.472,0.887c1.106,0.523,1.893,1.053,2.364,1.592C18.312,30.881,18.547,31.552,18.547,32.354z"
id="path14" />
<path
fill="#FFFFFF"
d="M31.274,29.682c0,1.391-0.317,2.599-0.949,3.621c-0.633,1.023-1.523,1.74-2.672,2.153l3.431,3.176H27.62 l-2.45-2.747c-1.05-0.038-1.998-0.316-2.842-0.833c-0.844-0.516-1.496-1.225-1.955-2.124s-0.689-1.902-0.689-3.007 c0-1.226,0.249-2.319,0.746-3.279c0.498-0.96,1.197-1.698,2.099-2.215c0.902-0.516,1.935-0.775,3.102-0.775 c1.088,0,2.063,0.25,2.924,0.751c0.86,0.5,1.528,1.212,2.004,2.136C31.036,27.463,31.274,28.511,31.274,29.682z M28.47,29.831 c0-1.199-0.261-2.146-0.784-2.842s-1.237-1.044-2.145-1.044c-0.924,0-1.663,0.349-2.219,1.047c-0.555,0.699-0.833,1.628-0.833,2.788 c0,1.155,0.272,2.077,0.816,2.767c0.545,0.69,1.267,1.035,2.169,1.035c0.919,0,1.647-0.334,2.186-1.002 C28.2,31.913,28.47,30.996,28.47,29.831z"
id="path16" />
<polygon
fill="#FFFFFF"
points="40.273,35.679 33.229,35.679 33.229,23.851 35.893,23.851 35.893,33.518 40.273,33.518 "
id="polygon18" />
<g
id="shape20-11-3"
transform="matrix(0.26020584,0,0,0.23675394,8.3683429,-32.192055)"
style="fill:#ff0000;fill-opacity:1"><title
id="title32-4">Sheet.20</title><path
d="m 0,200.91 h 18.19 l 16.25,-0.87 12.34,-44.32 c 0.65,-2.61 1.95,-7.83 7.15,-7.83 5.2,0 5.85,5.22 5.85,7.83 l 0.65,73.88 9.09,-25.21 c 0.65,-2.61 2.6,-3.48 3.9,-3.48 h 52.63 c 2.6,0 4.55,3.48 4.55,6.09 0,2.61 -1.95,7.82 -4.55,7.82 H 78.62 l -16.89,45.2 c -1.3,4.34 -2.6,6.08 -6.5,5.21 -1.95,-0.87 -3.9,-4.34 -3.9,-6.95 l -0.65,-67.8 -5.2,18.26 c -0.65,2.6 -1.95,6.08 -3.9,6.08 H 25.99 5.85 L 2.6,207.87 Z"
class="st4"
id="path34-3"
style="fill:#ff0000;fill-opacity:1" /></g></svg>

После

Ширина:  |  Высота:  |  Размер: 4.7 KiB

Просмотреть файл

@ -0,0 +1,15 @@
<svg id="f2f04349-8aee-4413-84c9-a9053611b319" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<defs>
<linearGradient id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74" x1="9" y1="15.83" x2="9" y2="5.79" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b3b3b3"/>
<stop offset="0.26" stop-color="#c1c1c1"/>
<stop offset="1" stop-color="#e6e6e6"/>
</linearGradient>
</defs>
<title>Icon-storage-86</title>
<path d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z" fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"/>
<path d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z" fill="#37c2b1"/>
<path d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z" fill="#fff"/>
<path d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z" fill="#37c2b1"/>
<path d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z" fill="#258277"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Просмотреть файл

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="f2f04349-8aee-4413-84c9-a9053611b319"
width="18"
height="18"
viewBox="0 0 18 18"
version="1.1"
sodipodi:docname="storageAccountAttached.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)">
<metadata
id="metadata26">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2160"
inkscape:window-height="1470"
id="namedview24"
showgrid="false"
inkscape:zoom="40.833333"
inkscape:cx="9"
inkscape:cy="9"
inkscape:window-x="85"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="f2f04349-8aee-4413-84c9-a9053611b319" />
<defs
id="defs9">
<linearGradient
id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74"
x1="9"
y1="15.83"
x2="9"
y2="5.79"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#b3b3b3"
id="stop2" />
<stop
offset="0.26"
stop-color="#c1c1c1"
id="stop4" />
<stop
offset="1"
stop-color="#e6e6e6"
id="stop6" />
</linearGradient>
</defs>
<title
id="title11">Icon-storage-86</title>
<path
d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z"
fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"
id="path13" />
<path
d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z"
fill="#37c2b1"
id="path15" />
<path
d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z"
fill="#fff"
id="path17" />
<path
d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z"
fill="#37c2b1"
id="path19" />
<path
d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z"
fill="#258277"
id="path21" />
<g
id="shape20-11-3"
transform="matrix(0.08637583,0,0,0.08390308,3.4299469,-8.1252329)"
style="fill:#ff0000;fill-opacity:1">
<title
id="title32-4">Sheet.20</title>
<path
d="m 0,200.91 h 18.19 l 16.25,-0.87 12.34,-44.32 c 0.65,-2.61 1.95,-7.83 7.15,-7.83 5.2,0 5.85,5.22 5.85,7.83 l 0.65,73.88 9.09,-25.21 c 0.65,-2.61 2.6,-3.48 3.9,-3.48 h 52.63 c 2.6,0 4.55,3.48 4.55,6.09 0,2.61 -1.95,7.82 -4.55,7.82 H 78.62 l -16.89,45.2 c -1.3,4.34 -2.6,6.08 -6.5,5.21 -1.95,-0.87 -3.9,-4.34 -3.9,-6.95 l -0.65,-67.8 -5.2,18.26 c -0.65,2.6 -1.95,6.08 -3.9,6.08 H 25.99 5.85 L 2.6,207.87 Z"
class="st4"
id="path34-3"
style="fill:#ff0000;fill-opacity:1" />
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 3.5 KiB

Просмотреть файл

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="f2f04349-8aee-4413-84c9-a9053611b319"
width="18"
height="18"
viewBox="0 0 18 18"
version="1.1"
sodipodi:docname="storageAccountV2.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)">
<metadata
id="metadata26">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1437"
inkscape:window-height="978"
id="namedview24"
showgrid="false"
inkscape:zoom="40.833333"
inkscape:cx="9"
inkscape:cy="9"
inkscape:window-x="56"
inkscape:window-y="-6"
inkscape:window-maximized="1"
inkscape:current-layer="f2f04349-8aee-4413-84c9-a9053611b319" />
<defs
id="defs9">
<linearGradient
id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74"
x1="9"
y1="15.83"
x2="9"
y2="5.79"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#b3b3b3"
id="stop2" />
<stop
offset="0.26"
stop-color="#c1c1c1"
id="stop4" />
<stop
offset="1"
stop-color="#e6e6e6"
id="stop6" />
</linearGradient>
<rect
x="32.433697"
y="-249.47739"
width="16.053041"
height="14.25117"
id="rect37" />
<rect
x="32.433697"
y="-249.47739"
width="16.053041"
height="14.25117"
id="rect65" />
</defs>
<title
id="title11">Icon-storage-86</title>
<path
d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z"
fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"
id="path13" />
<path
d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z"
fill="#37c2b1"
id="path15" />
<path
d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z"
fill="#fff"
id="path17" />
<path
d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z"
fill="#37c2b1"
id="path19" />
<path
d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z"
fill="#258277"
id="path21" />
<text
xml:space="preserve"
id="text35"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect37);fill:#ffffff;fill-opacity:1;stroke:none;"
transform="matrix(0.83442828,0,0,0.8035861,-20.993044,208.24026)"><tspan
x="32.433594"
y="-239.8211"><tspan
style="fill:#ffffff">V2</tspan></tspan></text>
</svg>

После

Ширина:  |  Высота:  |  Размер: 3.5 KiB

Просмотреть файл

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="f2f04349-8aee-4413-84c9-a9053611b319"
width="18"
height="18"
viewBox="0 0 18 18"
version="1.1"
sodipodi:docname="storageAccountV2Attached.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)">
<metadata
id="metadata26">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1437"
inkscape:window-height="978"
id="namedview24"
showgrid="false"
inkscape:zoom="40.833333"
inkscape:cx="9"
inkscape:cy="9"
inkscape:window-x="56"
inkscape:window-y="-6"
inkscape:window-maximized="1"
inkscape:current-layer="f2f04349-8aee-4413-84c9-a9053611b319"
inkscape:document-rotation="0" />
<defs
id="defs9">
<linearGradient
id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74"
x1="9"
y1="15.83"
x2="9"
y2="5.79"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#b3b3b3"
id="stop2" />
<stop
offset="0.26"
stop-color="#c1c1c1"
id="stop4" />
<stop
offset="1"
stop-color="#e6e6e6"
id="stop6" />
</linearGradient>
<rect
x="32.433697"
y="-249.47739"
width="16.053041"
height="14.25117"
id="rect37" />
<rect
x="32.433697"
y="-249.47739"
width="16.053041"
height="14.25117"
id="rect100" />
</defs>
<title
id="title11">Icon-storage-86</title>
<path
d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z"
fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"
id="path13" />
<path
d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z"
fill="#37c2b1"
id="path15" />
<path
d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z"
fill="#fff"
id="path17" />
<path
d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z"
fill="#37c2b1"
id="path19" />
<path
d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z"
fill="#258277"
id="path21" />
<text
xml:space="preserve"
id="text35"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect37);fill:#ffffff;fill-opacity:1;stroke:none;"
transform="matrix(0.83442828,0,0,0.8035861,-21.099162,208.19127)"><tspan
x="32.433594"
y="-239.8211"><tspan
style="fill:#ffffff">V2</tspan></tspan></text>
<g
id="shape20-11-3"
transform="matrix(0.08637583,0,0,0.08390308,2.56374,-8.4993302)"
style="fill:#ff0000;fill-opacity:1">
<title
id="title32-4">Sheet.20</title>
<path
d="m 0,200.91 h 18.19 l 16.25,-0.87 12.34,-44.32 c 0.65,-2.61 1.95,-7.83 7.15,-7.83 5.2,0 5.85,5.22 5.85,7.83 l 0.65,73.88 9.09,-25.21 c 0.65,-2.61 2.6,-3.48 3.9,-3.48 h 52.63 c 2.6,0 4.55,3.48 4.55,6.09 0,2.61 -1.95,7.82 -4.55,7.82 H 78.62 l -16.89,45.2 c -1.3,4.34 -2.6,6.08 -6.5,5.21 -1.95,-0.87 -3.9,-4.34 -3.9,-6.95 l -0.65,-67.8 -5.2,18.26 c -0.65,2.6 -1.95,6.08 -3.9,6.08 H 25.99 5.85 L 2.6,207.87 Z"
class="st4"
id="path34-3"
style="fill:#ff0000;fill-opacity:1" />
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 4.2 KiB

Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-folder{fill:#dcb67a}.icon-vs-fg{fill:#f0eff1}.icon-vs-bg{fill:#424242}</style><path class="icon-canvas-transparent" d="M0 0h16v16H0V0z" id="canvas"/><path class="icon-vs-out" d="M14.996 9.418V10H16v1.352l-1.004.96v.188c0 .827-.673 1.5-1.5 1.5h-.266l-2.092 2H9.441l.961-2H1.5C.673 14 0 13.327 0 12.5v-10C0 1.673.673 1 1.5 1h8.11l1 2h2.886c.827 0 1.5.673 1.5 1.5V7H16v1.414l-1.004 1.004z" id="outline" style="display: none;"/><path class="icon-vs-fg" d="M2 3h6.374l.5 1H2V3z" id="iconBg" style="display: none;"/><g id="iconFg"><path class="icon-vs-bg" d="M12 8l-2 4h2.5L11 15l4-4h-3l3-3z"/><path class="icon-folder" d="M13.996 7V4.5a.5.5 0 0 0-.5-.5H9.992l-1-2H1.5a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h6.882l3-6h2.614zM2 4V3h6.374l.5 1H2z"/></g></svg>

После

Ширина:  |  Высота:  |  Размер: 894 B

Просмотреть файл

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 64 64"
enable-background="new 0 0 64 64"
xml:space="preserve"
sodipodi:docname="taskHub.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
id="metadata17"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs15" /><sodipodi:namedview
inkscape:document-rotation="0"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2160"
inkscape:window-height="1470"
id="namedview13"
showgrid="false"
inkscape:zoom="11.484375"
inkscape:cx="32"
inkscape:cy="32"
inkscape:window-x="85"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<g
id="Layer_1-5"
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
style="fill:#b4b4b4;fill-opacity:1"><g
id="Azure"
style="fill:#b4b4b4;fill-opacity:1" /><g
transform="translate(37.3575,-2500.2)"
id="BizTalk_Services"
style="fill:#b4b4b4;fill-opacity:1" /><g
id="Key_Vault"
style="fill:#b4b4b4;fill-opacity:1" /><g
id="Mobile_Engagement"
style="fill:#b4b4b4;fill-opacity:1" /><g
id="Office_subscription"
style="fill:#b4b4b4;fill-opacity:1"><g
id="Office_subscription_1_"
style="fill:#b4b4b4;fill-opacity:1" /></g></g><g
id="paths"
transform="matrix(1.9931036,0,0,1.9931036,-119.22001,-85.548973)"
style="fill:#c8c8c8;fill-opacity:0.60000002"><g
id="Access_control"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Azure_active_directory"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="API_Management"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Azure_automation"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Azure_SQL_database"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Azure_subscription"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Backup_service"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Bitbucket_code_source"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Azure_cache"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Content_delivery_network__x28_CDN_x29_"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Cloud_service"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="CodePlex"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Dropbox_code_source"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Express_route"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Git_repository"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="GitHub_code"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="HD_Insight"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Health_monitoring"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Healthy"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="BizTalk_hybrid_connection"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Hybrid_connection_manager_for_BizTalk_hybrid_connection"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Hyper-V_recovery_manager"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Machine_learning"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Media_services"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Microsoft_account"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Mobile_services"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Multi-factor_authentication"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="MySQL_database"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Notification_hub"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Notification_topic"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Cloud_Office_365"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Office_365"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="OS_image"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Remote_app"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Task_scheduler"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Azure_SDK"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Service_bus"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Service_bus_queue"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Service_bus_relay"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Service_bus_topic"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Service_endpoint"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Custom_create"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="SQL_data_sync"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="SQL_reporting"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Startup_task"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Windows_Azure_storage"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Storage_blob"
style="fill:#c8c8c8;fill-opacity:0.60000002" /><g
id="Storage_table"
style="fill:#c8c8c8;fill-opacity:0.60000002"><path
id="path56"
d="m 85.5,42.3 h -19 L 57,58.7 66.5,75.1 h 18.9 l 9.5,-16.4 z m -10.4,8.6 h 4.4 v 4.4 h -4.4 z m 0,5.5 h 4.4 v 4.4 h -4.4 z m 0,5.5 h 4.4 v 4.4 h -4.4 z m -5.5,-11 H 74 v 4.4 h -4.4 z m 0,5.5 H 74 v 4.4 h -4.4 z m 0,5.5 H 74 v 4.4 h -4.4 z m 15.3,6.8 H 67 V 50.8 h 1.2 v 16.7 0 0 H 84.9 Z M 85,66.3 H 80.6 V 61.9 H 85 Z m 0,-5.5 H 80.6 V 56.4 H 85 Z m 0,-5.5 H 80.6 V 50.9 H 85 Z"
fill="#0078d7"
style="fill:#c8c8c8;fill-opacity:0.60000002" /></g></g><g
id="Layer_57"
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
style="fill:#b4b4b4;fill-opacity:1" /><g
id="Ibiza_Symbols"
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
style="fill:#b4b4b4;fill-opacity:1"><g
id="SQL_Database_Premium"
style="fill:#b4b4b4;fill-opacity:1" /></g><g
id="g10">
<path
fill="#3999C6"
d="M63.6,32.4c0.6-0.6,0.5-1.7,0-2.3L60.5,27L46.7,13.6c-0.6-0.6-1.5-0.6-2.2,0l0,0c-0.6,0.6-0.8,1.7,0,2.3 L59,30.1c0.6,0.6,0.6,1.7,0,2.3L44.2,47.1c-0.6,0.6-0.6,1.7,0,2.3l0,0c0.6,0.6,1.7,0.5,2.2,0l13.7-13.6c0,0,0,0,0.1-0.1L63.6,32.4z "
id="path2" />
<path
fill="#3999C6"
d="M0.4,32.4c-0.6-0.6-0.5-1.7,0-2.3L3.5,27l13.8-13.4c0.6-0.6,1.5-0.6,2.2,0l0,0c0.6,0.6,0.8,1.7,0,2.3 L5.3,30.1c-0.6,0.6-0.6,1.7,0,2.3l14.5,14.7c0.6,0.6,0.6,1.7,0,2.3l0,0c-0.6,0.6-1.7,0.5-2.2,0L3.6,36c0,0,0,0-0.1-0.1L0.4,32.4z"
id="path4" />
<polygon
fill="#FCD116"
points="47.6,2.5 28.1,2.5 17.6,32.1 30.4,32.2 20.4,61.5 48,22.4 34.6,22.4 "
id="polygon6" />
<polygon
opacity="0.3"
fill="#FF8C00"
enable-background="new "
points="34.6,22.4 47.6,2.5 37.4,2.5 26.6,27.1 39.4,27.2 20.4,61.5 48,22.4 "
id="polygon8" />
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 7.7 KiB

Просмотреть файл

@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="taskHubConnected.svg"
xml:space="preserve"
enable-background="new 0 0 64 64"
viewBox="0 0 64 64"
y="0px"
x="0px"
id="Layer_1"
version="1.1"><metadata
id="metadata17"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs15" /><sodipodi:namedview
inkscape:current-layer="Layer_1"
inkscape:window-maximized="1"
inkscape:window-y="-11"
inkscape:window-x="85"
inkscape:cy="44.509449"
inkscape:cx="32"
inkscape:zoom="8.1206794"
showgrid="false"
id="namedview13"
inkscape:window-height="1470"
inkscape:window-width="2160"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:document-rotation="0" />
<g
style="fill:#b4b4b4;fill-opacity:1"
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
id="Layer_1-5"><g
style="fill:#b4b4b4;fill-opacity:1"
id="Azure" /><g
style="fill:#b4b4b4;fill-opacity:1"
id="BizTalk_Services"
transform="translate(37.3575,-2500.2)" /><g
style="fill:#b4b4b4;fill-opacity:1"
id="Key_Vault" /><g
style="fill:#b4b4b4;fill-opacity:1"
id="Mobile_Engagement" /><g
style="fill:#b4b4b4;fill-opacity:1"
id="Office_subscription"><g
style="fill:#b4b4b4;fill-opacity:1"
id="Office_subscription_1_" /></g></g><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
transform="matrix(1.9931036,0,0,1.9931036,-119.22001,-85.548973)"
id="paths"><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Access_control" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Azure_active_directory" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="API_Management" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Azure_automation" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Azure_SQL_database" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Azure_subscription" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Backup_service" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Bitbucket_code_source" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Azure_cache" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Content_delivery_network__x28_CDN_x29_" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Cloud_service" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="CodePlex" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Dropbox_code_source" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Express_route" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Git_repository" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="GitHub_code" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="HD_Insight" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Health_monitoring" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Healthy" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="BizTalk_hybrid_connection" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Hybrid_connection_manager_for_BizTalk_hybrid_connection" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Hyper-V_recovery_manager" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Machine_learning" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Media_services" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Microsoft_account" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Mobile_services" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Multi-factor_authentication" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="MySQL_database" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Notification_hub" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Notification_topic" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Cloud_Office_365" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Office_365" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="OS_image" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Remote_app" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Task_scheduler" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Azure_SDK" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Service_bus" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Service_bus_queue" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Service_bus_relay" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Service_bus_topic" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Service_endpoint" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Custom_create" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="SQL_data_sync" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="SQL_reporting" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Startup_task" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Windows_Azure_storage" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Storage_blob" /><g
style="fill:#c8c8c8;fill-opacity:0.60000002"
id="Storage_table"><path
style="fill:#c8c8c8;fill-opacity:0.60000002"
fill="#0078d7"
d="m 85.5,42.3 h -19 L 57,58.7 66.5,75.1 h 18.9 l 9.5,-16.4 z m -10.4,8.6 h 4.4 v 4.4 h -4.4 z m 0,5.5 h 4.4 v 4.4 h -4.4 z m 0,5.5 h 4.4 v 4.4 h -4.4 z m -5.5,-11 H 74 v 4.4 h -4.4 z m 0,5.5 H 74 v 4.4 h -4.4 z m 0,5.5 H 74 v 4.4 h -4.4 z m 15.3,6.8 H 67 V 50.8 h 1.2 v 16.7 0 0 H 84.9 Z M 85,66.3 H 80.6 V 61.9 H 85 Z m 0,-5.5 H 80.6 V 56.4 H 85 Z m 0,-5.5 H 80.6 V 50.9 H 85 Z"
id="path56" /></g></g><g
style="fill:#b4b4b4;fill-opacity:1"
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
id="Layer_57" /><g
style="fill:#b4b4b4;fill-opacity:1"
transform="matrix(1.4141364,0,0,1.4141364,-75.161216,-51.749942)"
id="Ibiza_Symbols"><g
style="fill:#b4b4b4;fill-opacity:1"
id="SQL_Database_Premium" /></g><g
id="g10">
<path
id="path2"
d="M63.6,32.4c0.6-0.6,0.5-1.7,0-2.3L60.5,27L46.7,13.6c-0.6-0.6-1.5-0.6-2.2,0l0,0c-0.6,0.6-0.8,1.7,0,2.3 L59,30.1c0.6,0.6,0.6,1.7,0,2.3L44.2,47.1c-0.6,0.6-0.6,1.7,0,2.3l0,0c0.6,0.6,1.7,0.5,2.2,0l13.7-13.6c0,0,0,0,0.1-0.1L63.6,32.4z "
fill="#3999C6" />
<path
id="path4"
d="M0.4,32.4c-0.6-0.6-0.5-1.7,0-2.3L3.5,27l13.8-13.4c0.6-0.6,1.5-0.6,2.2,0l0,0c0.6,0.6,0.8,1.7,0,2.3 L5.3,30.1c-0.6,0.6-0.6,1.7,0,2.3l14.5,14.7c0.6,0.6,0.6,1.7,0,2.3l0,0c-0.6,0.6-1.7,0.5-2.2,0L3.6,36c0,0,0,0-0.1-0.1L0.4,32.4z"
fill="#3999C6" />
<polygon
id="polygon6"
points="47.6,2.5 28.1,2.5 17.6,32.1 30.4,32.2 20.4,61.5 48,22.4 34.6,22.4 "
fill="#FCD116" />
<polygon
id="polygon8"
points="34.6,22.4 47.6,2.5 37.4,2.5 26.6,27.1 39.4,27.2 20.4,61.5 48,22.4 "
enable-background="new "
fill="#FF8C00"
opacity="0.3" />
</g>
<g
style="fill:#ff0000;fill-opacity:1"
transform="matrix(0.3136085,0,0,0.28473247,12.242084,-28.645509)"
id="shape20-11-3"><title
id="title32-4">Sheet.20</title><path
style="fill:#ff0000;fill-opacity:1"
id="path34-3"
class="st4"
d="m 0,200.91 h 18.19 l 16.25,-0.87 12.34,-44.32 c 0.65,-2.61 1.95,-7.83 7.15,-7.83 5.2,0 5.85,5.22 5.85,7.83 l 0.65,73.88 9.09,-25.21 c 0.65,-2.61 2.6,-3.48 3.9,-3.48 h 52.63 c 2.6,0 4.55,3.48 4.55,6.09 0,2.61 -1.95,7.82 -4.55,7.82 H 78.62 l -16.89,45.2 c -1.3,4.34 -2.6,6.08 -6.5,5.21 -1.95,-0.87 -3.9,-4.34 -3.9,-6.95 l -0.65,-67.8 -5.2,18.26 c -0.65,2.6 -1.95,6.08 -3.9,6.08 H 25.99 5.85 L 2.6,207.87 Z" /></g></svg>

После

Ширина:  |  Высота:  |  Размер: 8.3 KiB

Просмотреть файл

@ -0,0 +1,298 @@
const portscanner = require('portscanner');
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import * as crypto from 'crypto';
import * as killProcessTree from 'tree-kill';
import axios from 'axios';
import { spawn, spawnSync, ChildProcess } from 'child_process';
import * as CryptoJS from 'crypto-js';
import { ConnStringUtils } from "./ConnStringUtils";
import * as SharedConstants from './SharedConstants';
import { Settings } from './Settings';
// Responsible for running the backend process
export class BackendProcess {
constructor(private _binariesFolder: string,
private _storageConnectionSettings: StorageConnectionSettings,
private _removeMyselfFromList: () => void,
private _log: (l: string) => void)
{ }
// Underlying Storage Connection Strings
get storageConnectionStrings(): string[] {
return this._storageConnectionSettings.storageConnStrings;
}
// Information about the started backend (if it was successfully started)
get backendUrl(): string {
return this._backendUrl;
}
// Folder where backend is run from (might be different, if the backend needs to be published first)
get binariesFolder(): string {
return this._eventualBinariesFolder;
}
// Kills the pending backend process
cleanup(): Promise<any> {
this._backendPromise = null;
this._backendUrl = '';
if (!this._funcProcess) {
return Promise.resolve();
}
console.log('Killing func process...');
return new Promise((resolve) => {
// The process is a shell. So to stop func.exe, we need to kill the entire process tree.
killProcessTree(this._funcProcess!.pid, resolve);
this._funcProcess = null;
});
}
get backendCommunicationNonce(): string { return this._backendCommunicationNonce; }
// Ensures that the backend is running (starts it, if needed) and returns its properties
getBackend(): Promise<void> {
if (!!this._backendPromise) {
return this._backendPromise;
}
this._backendPromise = new Promise<void>((resolve, reject) => {
vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: `Starting the backend `,
cancellable: true
}, (progress, token) => new Promise(stopProgress => {
// Starting the backend on a first available port
portscanner.findAPortNotInUse(37072, 38000).then((portNr: number) => {
const backendUrl = Settings().backendBaseUrl.replace('{portNr}', portNr.toString());
progress.report({ message: backendUrl });
// Now running func.exe in backend folder
this.startBackendOnPort(portNr, backendUrl, token)
.then(resolve, reject)
.finally(() => stopProgress(undefined));
}, (err: any) => { stopProgress(undefined); reject(`Failed to choose port for backend: ${err.message}`); });
}));
});
// Allowing the user to try again
this._backendPromise.catch(() => {
// This call is important, without it a typo in connString would persist until vsCode restart
this._removeMyselfFromList();
});
return this._backendPromise;
}
// Reference to the shell instance running func.exe
private _funcProcess: ChildProcess | null = null;
// Promise that resolves when the backend is started successfully
private _backendPromise: Promise<void> | null = null;
// Information about the started backend (if it was successfully started)
private _backendUrl: string = '';
// Folder where backend is run from (might be different, if the backend needs to be published first)
private _eventualBinariesFolder: string = this._binariesFolder;
// A nonce for communicating with the backend
private _backendCommunicationNonce = crypto.randomBytes(64).toString('base64');
// Runs the backend Function instance on some port
private startBackendOnPort(portNr: number, backendUrl: string, cancelToken: vscode.CancellationToken): Promise<void> {
return new Promise<void>((resolve, reject) => {
this._log(`Attempting to start the backend from ${this._binariesFolder} on ${backendUrl}...`);
if (!fs.existsSync(this._binariesFolder)) {
reject(`Couldn't find backend binaries in ${this._binariesFolder}`);
return;
}
// If this is a source code project
if (fs.readdirSync(this._binariesFolder).some(fn => fn.toLowerCase().endsWith('.csproj'))) {
const publishFolder = path.join(this._binariesFolder, 'publish');
// if it wasn't published yet
if (!fs.existsSync(publishFolder)) {
// publishing it
const publishProcess = spawnSync('dotnet', ['publish', '-o', publishFolder],
{ cwd: this._binariesFolder, encoding: 'utf8' }
);
if (!!publishProcess.stdout) {
this._log(publishProcess.stdout.toString());
}
if (publishProcess.status !== 0) {
const err = 'dotnet publish failed. ' +
(!!publishProcess.stderr ? publishProcess.stderr.toString() : `status: ${publishProcess.status}`);
this._log(`ERROR: ${err}`);
reject(err);
return;
}
}
this._eventualBinariesFolder = publishFolder;
}
// Important to inherit the context from VsCode, so that globally installed tools can be found
const env = process.env;
env[SharedConstants.NonceEnvironmentVariableName] = this._backendCommunicationNonce;
// Also setting AzureWebJobsSecretStorageType to 'files', so that the backend doesn't need Azure Storage
env['AzureWebJobsSecretStorageType'] = 'files';
if (this._storageConnectionSettings.isMsSql) {
env[SharedConstants.MsSqlConnStringEnvironmentVariableName] = this._storageConnectionSettings.storageConnStrings[0];
// For MSSQL just need to set DFM_HUB_NAME to something, doesn't matter what it is so far
env[SharedConstants.HubNameEnvironmentVariableName] = this._storageConnectionSettings.hubName;
} else {
// Need to unset this, in case it was set previously
delete env[SharedConstants.HubNameEnvironmentVariableName];
env['AzureWebJobsStorage'] = this._storageConnectionSettings.storageConnStrings[0];
}
this._funcProcess = spawn('func', ['start', '--port', portNr.toString(), '--csharp'], {
cwd: this._eventualBinariesFolder,
shell: true,
env
});
this._funcProcess.stdout.on('data', (data) => {
const msg = data.toString();
this._log(msg);
if (msg.toLowerCase().includes('no valid combination of account information found')) {
reject('The provided Storage Connection String and/or Hub Name seem to be invalid.');
}
});
this._funcProcess!.stderr.on('data', (data) => {
const msg = data.toString();
this._log(`ERROR: ${msg}`);
reject(`Func: ${msg}`);
});
console.log(`Waiting for ${backendUrl} to respond...`);
// Waiting for the backend to be ready
const timeoutInSeconds = Settings().backendTimeoutInSeconds;
const intervalInMs = 500, numOfTries = timeoutInSeconds * 1000 / intervalInMs;
var i = numOfTries;
const intervalToken = setInterval(() => {
const headers: any = {};
headers[SharedConstants.NonceHeaderName] = this._backendCommunicationNonce;
// Pinging the backend and returning its URL when ready
axios.get(`${backendUrl}/--${this._storageConnectionSettings.hubName}/about`, { headers }).then(response => {
console.log(`The backend is now running on ${backendUrl}`);
clearInterval(intervalToken);
this._backendUrl = backendUrl;
resolve();
}, err => {
if (!!err.response && err.response.status === 401) {
// This typically happens when mistyping Task Hub name
clearInterval(intervalToken);
reject(err.message);
}
});
if (cancelToken.isCancellationRequested) {
clearInterval(intervalToken);
reject(`Cancelled by the user`);
} else if (--i <= 0) {
console.log(`Timed out waiting for the backend!`);
clearInterval(intervalToken);
reject(`No response within ${timeoutInSeconds} seconds. Ensure you have the latest Azure Functions Core Tools installed globally.`);
}
}, intervalInMs);
});
}
}
export class StorageConnectionSettings {
get storageConnStrings(): string[] { return this._connStrings; };
get hubName(): string { return this._hubName; };
get connStringHashKey(): string { return this._connStringHashKey; }
get hashKey(): string { return this._hashKey; }
get isFromLocalSettingsJson(): boolean { return this._fromLocalSettingsJson; }
get isMsSql(): boolean { return !!ConnStringUtils.GetSqlServerName(this._connStrings[0]); }
constructor(private _connStrings: string[],
private _hubName: string,
private _fromLocalSettingsJson: boolean = false) {
this._connStringHashKey = StorageConnectionSettings.GetConnStringHashKey(this._connStrings);
this._hashKey = this._connStringHashKey + this._hubName.toLowerCase();
}
static GetConnStringHashKey(connStrings: string[]): string {
const sqlServerName = ConnStringUtils.GetSqlServerName(connStrings[0]);
if (!!sqlServerName) {
return sqlServerName + ConnStringUtils.GetSqlDatabaseName(connStrings[0]);
}
return ConnStringUtils.GetTableEndpoint(connStrings[0]).toLowerCase();
}
static MaskStorageConnString(connString: string): string {
return connString.replace(/AccountKey=[^;]+/gi, 'AccountKey=*****');
}
private readonly _connStringHashKey: string;
private readonly _hashKey: string;
}
// Creates the SharedKeyLite signature to query Table Storage REST API, also adds other needed headers
export function CreateAuthHeadersForTableStorage(accountName: string, accountKey: string, queryUrl: string): {} {
const dateInUtc = new Date().toUTCString();
const signature = CryptoJS.HmacSHA256(`${dateInUtc}\n/${accountName}/${queryUrl}`, CryptoJS.enc.Base64.parse(accountKey));
return {
'Authorization': `SharedKeyLite ${accountName}:${signature.toString(CryptoJS.enc.Base64)}`,
'x-ms-date': dateInUtc,
'x-ms-version': '2015-12-11',
'Accept': 'application/json;odata=nometadata'
};
}

Просмотреть файл

@ -0,0 +1,79 @@
import { Settings } from './Settings';
export class ConnStringUtils {
// Extracts AccountName from Storage Connection String
static GetAccountName(connString: string): string {
const match = /AccountName=([^;]+)/i.exec(connString);
return (!!match && match.length > 0) ? match[1] : '';
}
// Extracts AccountKey from Storage Connection String
static GetAccountKey(connString: string): string {
const match = /AccountKey=([^;]+)/i.exec(connString);
return (!!match && match.length > 0) ? match[1] : '';
}
// Extracts DefaultEndpointsProtocol from Storage Connection String
static GetDefaultEndpointsProtocol(connString: string): string {
const match = /DefaultEndpointsProtocol=([^;]+)/i.exec(connString);
return (!!match && match.length > 0) ? match[1] : 'https';
}
// Extracts TableEndpoint from Storage Connection String
static GetTableEndpoint(connString: string): string {
const accountName = ConnStringUtils.GetAccountName(connString);
if (!accountName) {
return '';
}
const endpointsProtocol = ConnStringUtils.GetDefaultEndpointsProtocol(connString);
const suffixMatch = /EndpointSuffix=([^;]+)/i.exec(connString);
if (!!suffixMatch && suffixMatch.length > 0) {
return `${endpointsProtocol}://${accountName}.table.${suffixMatch[1]}/`;
}
const endpointMatch = /TableEndpoint=([^;]+)/i.exec(connString);
return (!!endpointMatch && endpointMatch.length > 0) ? endpointMatch[1] : `${endpointsProtocol}://${accountName}.table.core.windows.net/`;
}
// Replaces 'UseDevelopmentStorage=true' with full Storage Emulator connection string
static ExpandEmulatorShortcutIfNeeded(connString: string): string {
if (connString.includes('UseDevelopmentStorage=true')) {
return Settings().storageEmulatorConnectionString;
}
return connString;
}
// Extracts server name from MSSQL Connection String
static GetSqlServerName(connString: string): string {
const match = /(Data Source|Server)=([^;]+)/i.exec(connString);
return (!!match && match.length > 1) ? match[2] : '';
}
// Extracts database name from MSSQL Connection String
static GetSqlDatabaseName(connString: string): string {
const match = /Initial Catalog=([^;]+)/i.exec(connString);
return (!!match && match.length > 0) ? match[1] : '';
}
// Extracts human-readable storage name from a bunch of connection strings
static GetStorageName(connStrings: string[]): string {
const serverName = this.GetSqlServerName(connStrings[0]);
if (!serverName) {
return this.GetAccountName(connStrings[0]);
}
const dbName = this.GetSqlDatabaseName(connStrings[0]);
return serverName + (!dbName ? '' : '/' + dbName);
}
}

Просмотреть файл

@ -0,0 +1,120 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as rimraf from 'rimraf';
import { FunctionGraphView } from "./FunctionGraphView";
import { traverseFunctionProject } from './az-func-as-a-graph/traverseFunctionProject';
import { FunctionsMap, ProxiesMap } from './az-func-as-a-graph/FunctionsMap';
export type TraversalResult = {
functions: FunctionsMap;
proxies: ProxiesMap;
};
// Aggregates Function Graph views
export class FunctionGraphList {
constructor(private _context: vscode.ExtensionContext, logChannel?: vscode.OutputChannel) {
this._log = !logChannel ? (s: any) => { } : (s: any) => logChannel!.append(s);
}
traverseFunctions(projectPath: string): Promise<TraversalResult> {
const isCurrentProject = projectPath === vscode.workspace.rootPath;
if (isCurrentProject && !!this._traversalResult) {
return Promise.resolve(this._traversalResult);
}
return traverseFunctionProject(projectPath, this._log).then(result => {
this._tempFolders.push(...result.tempFolders);
// Caching current project's functions
if (isCurrentProject) {
this._traversalResult = { functions: result.functions, proxies: result.proxies };
// And cleanup the cache on any change to the file system
if (!!this._watcher) {
this._watcher.dispose();
}
this._watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(projectPath, '**/*'));
const cacheCleanupRoutine = () => {
this._traversalResult = undefined;
if (!!this._watcher) {
this._watcher.dispose();
this._watcher = undefined;
}
}
this._watcher.onDidCreate(cacheCleanupRoutine);
this._watcher.onDidDelete(cacheCleanupRoutine);
this._watcher.onDidChange(cacheCleanupRoutine);
}
return { functions: result.functions, proxies: result.proxies };
});
}
visualize(item?: vscode.Uri): void {
// If host.json was clicked
if (!!item && item.scheme === 'file' && item.fsPath.toLowerCase().endsWith('host.json')) {
this.visualizeProjectPath(path.dirname(item.fsPath));
return;
}
var defaultProjectPath = '';
const ws = vscode.workspace;
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'host.json'))) {
defaultProjectPath = ws.rootPath;
}
vscode.window.showInputBox({ value: defaultProjectPath, prompt: 'Local path or link to GitHub repo' }).then(projectPath => {
if (!!projectPath) {
this.visualizeProjectPath(projectPath);
}
});
}
visualizeProjectPath(projectPath: string): void {
this._views.push(new FunctionGraphView(this._context, projectPath, this));
}
// Closes all views
cleanup(): void {
if (!!this._watcher) {
this._watcher.dispose();
this._watcher = undefined;
}
for (const view of this._views) {
view.cleanup();
}
for (var tempFolder of this._tempFolders) {
this._log(`Removing ${tempFolder}`);
try {
rimraf.sync(tempFolder)
} catch (err) {
this._log(`Failed to remove ${tempFolder}: ${err.message}`);
}
}
}
private _views: FunctionGraphView[] = [];
private _traversalResult?: TraversalResult;
private _watcher?: vscode.FileSystemWatcher;
private _tempFolders: string[] = [];
private _log: (line: string) => void;
}

Просмотреть файл

@ -0,0 +1,195 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { MonitorView } from './MonitorView';
import { FunctionGraphList, TraversalResult } from './FunctionGraphList';
// Represents the function graph view
export class FunctionGraphView
{
constructor(private _context: vscode.ExtensionContext,
private _functionProjectPath: string,
private _functionGraphList: FunctionGraphList) {
this._staticsFolder = path.join(this._context.extensionPath, 'backend', 'DfmStatics');
this._webViewPanel = this.showWebView();
}
// Closes this web view
cleanup(): void {
if (!!this._webViewPanel) {
this._webViewPanel.dispose();
}
}
// Path to html statics
private _staticsFolder: string;
// Reference to the already opened WebView with the main page
private _webViewPanel: vscode.WebviewPanel | null = null;
// Functions and proxies currently shown
private _traversalResult?: TraversalResult;
private static readonly ViewType = 'durableFunctionsMonitorFunctionGraph';
// Opens a WebView with function graph page in it
private showWebView(): vscode.WebviewPanel {
const title = `Functions Graph (${this._functionProjectPath})`;
const panel = vscode.window.createWebviewPanel(
FunctionGraphView.ViewType,
title,
vscode.ViewColumn.One,
{
retainContextWhenHidden: true,
enableScripts: true,
localResourceRoots: [vscode.Uri.file(this._staticsFolder)]
}
);
var html = fs.readFileSync(path.join(this._staticsFolder, 'index.html'), 'utf8');
html = MonitorView.fixLinksToStatics(html, this._staticsFolder, panel.webview);
html = this.embedTheme(html);
html = this.embedParams(html, !!this._functionProjectPath);
panel.webview.html = html;
// handle events from WebView
panel.webview.onDidReceiveMessage(request => {
switch (request.method) {
case 'SaveAs':
// Just to be extra sure...
if (!MonitorView.looksLikeSvg(request.data)) {
vscode.window.showErrorMessage(`Invalid data format. Save failed.`);
return;
}
// Saving some file to local hard drive
vscode.window.showSaveDialog({ filters: { 'SVG Images': ['svg'] } }).then(filePath => {
if (!filePath || !filePath.fsPath) {
return;
}
fs.writeFile(filePath!.fsPath, request.data, err => {
if (!err) {
vscode.window.showInformationMessage(`Saved to ${filePath!.fsPath}`);
} else {
vscode.window.showErrorMessage(`Failed to save. ${err}`);
}
});
});
return;
case 'SaveFunctionGraphAsJson':
if (!this._traversalResult) {
return;
}
// Saving some file to local hard drive
vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file('dfm-func-map.json'), filters: { 'JSON': ['json'] } }).then(filePath => {
if (!filePath || !filePath.fsPath) {
return;
}
fs.writeFile(filePath!.fsPath, JSON.stringify(this._traversalResult, null, 3), err => {
if (!err) {
vscode.window.showInformationMessage(`Saved to ${filePath!.fsPath}`);
} else {
vscode.window.showErrorMessage(`Failed to save. ${err}`);
}
});
});
return;
case 'GotoFunctionCode':
if (!this._traversalResult) {
return;
}
const functionName = request.url;
var functionOrProxy: any = null;
if (functionName.startsWith('proxy.')) {
functionOrProxy = this._traversalResult.proxies[functionName.substr(6)];
} else {
functionOrProxy = this._traversalResult.functions[functionName];
}
vscode.window.showTextDocument(vscode.Uri.file(functionOrProxy.filePath)).then(ed => {
const pos = ed.document.positionAt(!!functionOrProxy.pos ? functionOrProxy.pos : 0);
ed.selection = new vscode.Selection(pos, pos);
ed.revealRange(new vscode.Range(pos, pos));
});
return;
}
// Intercepting request for Function Map
if (request.method === "GET" && request.url === '/function-map') {
if (!this._functionProjectPath) {
return;
}
const requestId = request.id;
this._functionGraphList.traverseFunctions(this._functionProjectPath).then(result => {
this._traversalResult = result;
panel.webview.postMessage({
id: requestId, data: {
functions: result.functions,
proxies: result.proxies
}
});
}, err => {
// err might fail to serialize here, so passing err.message only
panel.webview.postMessage({ id: requestId, err: { message: err.message } });
});
}
}, undefined, this._context.subscriptions);
return panel;
}
// Embeds the current color theme
private embedTheme(html: string): string {
if ([2, 3].includes((vscode.window as any).activeColorTheme.kind)) {
return html.replace('<script>var DfmClientConfig={}</script>', '<script>var DfmClientConfig={\'theme\':\'dark\'}</script>');
}
return html;
}
// Embeds some other parameters in the HTML served
private embedParams(html: string, isFunctionGraphAvailable: boolean): string {
return html
.replace(
`<script>var IsFunctionGraphAvailable=0</script>`,
`<script>var IsFunctionGraphAvailable=${!!isFunctionGraphAvailable ? 1 : 0}</script>`
)
.replace(
`<script>var DfmViewMode=0</script>`,
`<script>var DfmViewMode=1</script>`
);
}
}

Просмотреть файл

@ -0,0 +1,393 @@
import * as vscode from 'vscode';
import { MonitorView } from "./MonitorView";
import { MonitorViewList } from "./MonitorViewList";
import { StorageAccountTreeItem } from './StorageAccountTreeItem';
import { StorageAccountTreeItems } from './StorageAccountTreeItems';
import { TaskHubTreeItem } from './TaskHubTreeItem';
import { SubscriptionTreeItems } from './SubscriptionTreeItems';
import { SubscriptionTreeItem } from './SubscriptionTreeItem';
import { FunctionGraphList } from './FunctionGraphList';
import { Settings, UpdateSetting } from './Settings';
import { StorageConnectionSettings } from './BackendProcess';
// Root object in the hierarchy. Also serves data for the TreeView.
export class MonitorTreeDataProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
constructor(private _context: vscode.ExtensionContext, functionGraphList: FunctionGraphList, logChannel?: vscode.OutputChannel) {
this._monitorViews = new MonitorViewList(this._context,
functionGraphList,
() => this._onDidChangeTreeData.fire(),
!logChannel ? () => { } : (l) => logChannel.append(l));
const resourcesFolderPath = this._context.asAbsolutePath('resources');
this._storageAccounts = new StorageAccountTreeItems(resourcesFolderPath, this._monitorViews);
// Using Azure Account extension to connect to Azure, get subscriptions etc.
const azureAccountExtension = vscode.extensions.getExtension('ms-vscode.azure-account');
// Typings for azureAccount are here: https://github.com/microsoft/vscode-azure-account/blob/master/src/azure-account.api.d.ts
const azureAccount = !!azureAccountExtension ? azureAccountExtension.exports : undefined;
if (!!azureAccount && !!azureAccount.onFiltersChanged) {
// When user changes their list of filtered subscriptions (or just relogins to Azure)...
this._context.subscriptions.push(azureAccount.onFiltersChanged(() => this.refresh()));
}
this._subscriptions = new SubscriptionTreeItems(
this._context,
azureAccount,
this._storageAccounts,
() => this._onDidChangeTreeData.fire(),
resourcesFolderPath,
!logChannel ? () => { } : (l) => logChannel.appendLine(l)
);
// Also trying to parse current project's files and create a Task Hub node for them
const connSettingsFromCurrentProject = this._monitorViews.getStorageConnectionSettingsFromCurrentProject();
if (!!connSettingsFromCurrentProject) {
this._storageAccounts.addNodeForConnectionSettings(connSettingsFromCurrentProject);
}
}
// Does nothing, actually
getTreeItem(element: vscode.TreeItem): vscode.TreeItem { return element; }
// Returns the children of `element` or root if no element is passed.
getChildren(element?: vscode.TreeItem): Promise<vscode.TreeItem[]> {
if (!element) {
return this._subscriptions.getNonEmptyNodes();
}
const subscriptionNode = element as SubscriptionTreeItem;
if (subscriptionNode.isSubscriptionTreeItem) {
const storageAccountNodes = subscriptionNode.storageAccountNodes;
// Initially collapsing those storage nodes, that don't have attached TaskHubs at the moment
for (const n of storageAccountNodes) {
if (!n.isAttached) {
n.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
}
}
return Promise.resolve(storageAccountNodes);
}
// If this is a storage account tree item
const item = element as StorageAccountTreeItem;
if (this._storageAccounts.nodes.includes(item)) {
return Promise.resolve(item.childItems);
}
return Promise.resolve([]);
}
// Handles 'Attach' context menu item or a click on a tree node
attachToTaskHub(taskHubItem: TaskHubTreeItem | null, messageToWebView: any = undefined): void {
if (!!this._inProgress) {
console.log(`Another operation already in progress...`);
return;
}
// This could happen, if the command is executed via Command Palette (and not via menu)
if (!taskHubItem) {
this.createOrActivateMonitorView(false, messageToWebView);
return;
}
this._inProgress = true;
const monitorView = this._monitorViews.getOrCreateFromStorageConnectionSettings(taskHubItem.storageConnectionSettings);
monitorView.show(messageToWebView).then(() => {
this._onDidChangeTreeData.fire();
this._inProgress = false;
}, (err: any) => {
// .finally() doesn't work here - vscode.window.showErrorMessage() blocks it until user
// closes the error message. As a result, _inProgress remains true until then, which blocks all commands
this._inProgress = false;
vscode.window.showErrorMessage(!err.message ? err : err.message);
});
}
// Triggers when F5 is being hit
handleOnDebugSessionStarted() {
if (!!this._monitorViews.isAnyMonitorViewVisible()) {
return;
}
const DfmDoNotAskUponDebugSession = 'DfmDoNotAskUponDebugSession';
const doNotAsk = this._context.globalState.get(DfmDoNotAskUponDebugSession, false);
if (!Settings().showWhenDebugSessionStarts && !!doNotAsk) {
return;
}
const defaultTaskHubName = 'TestHubName';
const curConnSettings = this._monitorViews.getStorageConnectionSettingsFromCurrentProject(defaultTaskHubName);
if (!curConnSettings) {
return;
}
if (!Settings().showWhenDebugSessionStarts) {
const prompt = `Do you want Durable Functions Monitor to be automatically shown when you start debugging a Durable Functions project? You can always change this preference via Settings.`;
vscode.window.showWarningMessage(prompt, `Yes`, `No, and don't ask again`).then(answer => {
if (answer === `No, and don't ask again`) {
UpdateSetting('showWhenDebugSessionStarts', false);
this._context.globalState.update(DfmDoNotAskUponDebugSession, true);
} else if (answer === `Yes`) {
UpdateSetting('showWhenDebugSessionStarts', true);
this.showUponDebugSession(
curConnSettings.hubName !== defaultTaskHubName ? curConnSettings : undefined
);
}
});
} else {
this.showUponDebugSession(
curConnSettings.hubName !== defaultTaskHubName ? curConnSettings : undefined
);
}
}
// Handles 'Detach' context menu item
detachFromTaskHub(storageAccountItem: StorageAccountTreeItem) {
if (!storageAccountItem) {
vscode.window.showInformationMessage('This command is only available via context menu');
return;
}
if (!!this._inProgress) {
console.log(`Another operation already in progress...`);
return;
}
this._inProgress = true;
this._monitorViews.detachBackend(storageAccountItem.storageConnStrings).then(() => {
this._onDidChangeTreeData.fire();
this._inProgress = false;
}, err => {
this._inProgress = false;
vscode.window.showErrorMessage(`Failed to detach from Task Hub. ${err}`);
});
}
// Handles 'Delete Task Hub' context menu item
deleteTaskHub(taskHubItem: TaskHubTreeItem) {
if (!taskHubItem) {
vscode.window.showInformationMessage('This command is only available via context menu');
return;
}
if (!!this._inProgress) {
console.log(`Another operation already in progress...`);
return;
}
const monitorView = this._monitorViews.getOrCreateFromStorageConnectionSettings(taskHubItem.storageConnectionSettings);
if (!monitorView) {
console.log(`Tried to delete a detached Task Hub`);
return;
}
const prompt = `This will permanently delete all Azure Storage resources used by '${taskHubItem.label}' orchestration service. There should be no running Function instances for this Task Hub present. Are you sure you want to proceed?`;
vscode.window.showWarningMessage(prompt, 'Yes', 'No').then(answer => {
if (answer === 'Yes') {
this._inProgress = true;
monitorView.deleteTaskHub().then(() => {
taskHubItem.removeFromTree();
this._onDidChangeTreeData.fire();
this._inProgress = false;
}, (err) => {
this._inProgress = false;
vscode.window.showErrorMessage(`Failed to delete Task Hub. ${err}`);
});
}
});
}
// Handles 'Open in Storage Explorer' context menu item
async openTableInStorageExplorer(taskHubItem: TaskHubTreeItem, table: 'Instances' | 'History') {
// Using Azure Storage extension for this
var storageExt = vscode.extensions.getExtension('ms-azuretools.vscode-azurestorage');
if (!storageExt) {
vscode.window.showErrorMessage(`For this to work, please, install [Azure Storage](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurestorage) extension.`);
return;
}
try {
if (!storageExt.isActive) {
await storageExt.activate();
}
await vscode.commands.executeCommand('azureStorage.openTable', {
root: {
storageAccountId: taskHubItem.storageAccountId,
subscriptionId: taskHubItem.subscriptionId
},
tableName: taskHubItem.hubName + table
});
} catch (err) {
vscode.window.showErrorMessage(`Failed to execute command. ${err}`);
}
}
// Handles 'Attach' button
attachToAnotherTaskHub() {
this.createOrActivateMonitorView(true);
}
// Handles 'Refresh' button
refresh() {
this._subscriptions.cleanup();
this._onDidChangeTreeData.fire();
}
// Handles 'Detach from all Task Hubs' button
detachFromAllTaskHubs() {
if (!!this._inProgress) {
console.log(`Another operation already in progress...`);
return;
}
this._inProgress = true;
this.cleanup().catch(err => {
vscode.window.showErrorMessage(`Failed to detach from Task Hub. ${err}`);
}).finally(() => {
this._onDidChangeTreeData.fire();
this._inProgress = false;
});
}
// Handles 'Go to instanceId...' context menu item
gotoInstanceId(taskHubItem: TaskHubTreeItem | null) {
// Trying to get a running backend instance.
// If the relevant MonitorView is currently not visible, don't want to show it - that's why all the custom logic here.
var monitorView = !taskHubItem ?
this._monitorViews.firstOrDefault() :
this._monitorViews.getOrCreateFromStorageConnectionSettings(taskHubItem.storageConnectionSettings);
if (!!monitorView) {
monitorView.gotoInstanceId();
} else {
this.createOrActivateMonitorView(false).then(view => {
if (!!view) {
// Not sure why this timeout here is needed, but without it the quickPick isn't shown
setTimeout(() => {
view.gotoInstanceId();
}, 1000);
}
});
}
}
// Stops all backend processes and closes all views
cleanup(): Promise<any> {
return this._monitorViews.cleanup();
}
private _inProgress: boolean = false;
private _monitorViews: MonitorViewList;
private _storageAccounts: StorageAccountTreeItems;
private _subscriptions: SubscriptionTreeItems;
private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem | undefined> = new vscode.EventEmitter<StorageAccountTreeItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem | undefined> = this._onDidChangeTreeData.event;
// Shows or makes active the main view
private createOrActivateMonitorView(alwaysCreateNew: boolean, messageToWebView: any = undefined): Promise<MonitorView | null> {
if (!!this._inProgress) {
console.log(`Another operation already in progress...`);
return Promise.resolve(null);
}
return new Promise<MonitorView>((resolve, reject) => {
this._monitorViews.getOrAdd(alwaysCreateNew).then(monitorView => {
this._inProgress = true;
monitorView.show(messageToWebView).then(() => {
this._storageAccounts.addNodeForMonitorView(monitorView);
this._onDidChangeTreeData.fire();
this._inProgress = false;
resolve(monitorView);
}, (err) => {
// .finally() doesn't work here - vscode.window.showErrorMessage() blocks it until user
// closes the error message. As a result, _inProgress remains true until then, which blocks all commands
this._inProgress = false;
vscode.window.showErrorMessage(!err.message ? err : err.message);
});
}, vscode.window.showErrorMessage);
});
}
// Shows the main view upon a debug session
private showUponDebugSession(connSettingsFromCurrentProject?: StorageConnectionSettings) {
if (!!this._inProgress) {
console.log(`Another operation already in progress...`);
return;
}
this._monitorViews.showUponDebugSession(connSettingsFromCurrentProject).then(monitorView => {
this._inProgress = true;
monitorView.show().then(() => {
this._storageAccounts.addNodeForMonitorView(monitorView);
this._onDidChangeTreeData.fire();
this._inProgress = false;
}, (err) => {
// .finally() doesn't work here - vscode.window.showErrorMessage() blocks it until user
// closes the error message. As a result, _inProgress remains true until then, which blocks all commands
this._inProgress = false;
vscode.window.showErrorMessage(!err.message ? err : err.message);
});
}, vscode.window.showErrorMessage);
}
}

Просмотреть файл

@ -0,0 +1,424 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
import * as SharedConstants from './SharedConstants';
import { BackendProcess, StorageConnectionSettings } from './BackendProcess';
import { ConnStringUtils } from './ConnStringUtils';
import { Settings } from './Settings';
import { FunctionGraphList } from './FunctionGraphList';
// Represents the main view, along with all detailed views
export class MonitorView
{
// Storage Connection settings (connString and hubName) of this Monitor View
get storageConnectionSettings(): StorageConnectionSettings {
return new StorageConnectionSettings(this._backend.storageConnectionStrings, this._hubName);
}
get isVisible(): boolean {
return !!this._webViewPanel;
}
// Path to html statics
get staticsFolder(): string {
return path.join(this._backend.binariesFolder, 'DfmStatics');
}
constructor(private _context: vscode.ExtensionContext,
private _backend: BackendProcess,
private _hubName: string,
private _functionGraphList: FunctionGraphList,
private _onViewStatusChanged: () => void) {
const ws = vscode.workspace;
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'host.json'))) {
this._functionProjectPath = ws.rootPath;
}
}
// Closes all WebViews
cleanup(): void {
for (var childPanel of this._childWebViewPanels) {
childPanel.dispose();
}
this._childWebViewPanels = [];
if (!!this._webViewPanel) {
this._webViewPanel.dispose();
}
}
// Shows or makes active the main view
show(messageToWebView: any = undefined): Promise<void> {
if (!!this._webViewPanel) {
// Didn't find a way to check whether the panel still exists.
// So just have to catch a "panel disposed" exception here.
try {
this._webViewPanel.reveal();
if (!!messageToWebView) {
// BUG: WebView might actually appear in 3 states: disposed, visible and inactive.
// Didn't find the way to distinguish the last two.
// But when it is inactive, it will be activated with above reveal() method,
// and then miss this message we're sending here. No good solution for this problem so far...
this._webViewPanel.webview.postMessage(messageToWebView);
}
return Promise.resolve();
} catch (err) {
this._webViewPanel = null;
}
}
return new Promise<void>((resolve, reject) => {
this._backend.getBackend().then(() => {
try {
this._webViewPanel = this.showWebView('', messageToWebView);
this._webViewPanel.onDidDispose(() => {
this._webViewPanel = null;
this._onViewStatusChanged();
});
resolve();
} catch (err) {
reject(`WebView failed: ${err}`);
}
}, reject);
});
}
// Permanently deletes all underlying Storage resources for this Task Hub
deleteTaskHub(): Promise<void> {
if (!this._backend.backendUrl) {
return Promise.reject('Backend is not started');
}
const headers: any = {};
headers[SharedConstants.NonceHeaderName] = this._backend.backendCommunicationNonce;
return new Promise<void>((resolve, reject) => {
const url = `${this._backend.backendUrl}/--${this._hubName}/delete-task-hub`;
axios.post(url, {}, { headers }).then(() => {
this.cleanup();
resolve();
}, err => reject(err.message));
});
}
// Handles 'Goto instanceId...' context menu item
gotoInstanceId() {
this.askForInstanceId().then(instanceId => {
// Opening another WebView
this._childWebViewPanels.push(this.showWebView(instanceId));
});
}
// Converts script and CSS links
static fixLinksToStatics(originalHtml: string, pathToBackend: string, webView: vscode.Webview): string {
var resultHtml: string = originalHtml;
const regex = / (href|src)="\/([0-9a-z.\/]+)"/ig;
var match: RegExpExecArray | null;
while (match = regex.exec(originalHtml)) {
const relativePath = match[2];
const localPath = path.join(pathToBackend, relativePath);
const newPath = webView.asWebviewUri(vscode.Uri.file(localPath)).toString();
resultHtml = resultHtml.replace(`/${relativePath}`, newPath);
}
return resultHtml;
}
// Validates incoming SVG, just to be extra sure...
static looksLikeSvg(data: string): boolean {
return data.startsWith('<svg') && data.endsWith('</svg>') && !data.includes('<script');
}
// Reference to the already opened WebView with the main page
private _webViewPanel: vscode.WebviewPanel | null = null;
// Reference to all child WebViews
private _childWebViewPanels: vscode.WebviewPanel[] = [];
// Functions and proxies currently shown
private _functionsAndProxies: { [name: string]: { filePath?: string, pos?: number } } = {};
private _functionProjectPath: string = '';
private static readonly ViewType = 'durableFunctionsMonitor';
private static readonly GlobalStateName = MonitorView.ViewType + 'WebViewState';
// Opens a WebView with main page or orchestration page in it
private showWebView(orchestrationId: string = '', messageToWebView: any = undefined): vscode.WebviewPanel {
const title = (!!orchestrationId) ?
`Instance '${orchestrationId}'`
:
`Durable Functions Monitor (${this.taskHubFullTitle})`;
const panel = vscode.window.createWebviewPanel(
MonitorView.ViewType,
title,
vscode.ViewColumn.One,
{
retainContextWhenHidden: true,
enableScripts: true,
localResourceRoots: [vscode.Uri.file(this.staticsFolder)]
}
);
var html = fs.readFileSync(path.join(this.staticsFolder, 'index.html'), 'utf8');
html = MonitorView.fixLinksToStatics(html, this.staticsFolder, panel.webview);
// Also passing persisted settings via HTML
const webViewState = this._context.globalState.get(MonitorView.GlobalStateName, {});
html = this.embedOrchestrationIdAndState(html, orchestrationId, webViewState);
html = this.embedIsFunctionGraphAvailable(html, !!this._functionProjectPath);
html = this.embedThemeAndSettings(html);
panel.webview.html = html;
// handle events from WebView
panel.webview.onDidReceiveMessage(request => {
switch (request.method) {
case 'IAmReady':
// Sending an initial message (if any), when the webView is ready
if (!!messageToWebView) {
panel.webview.postMessage(messageToWebView);
messageToWebView = undefined;
}
return;
case 'PersistState':
// Persisting state values
const webViewState = this._context.globalState.get(MonitorView.GlobalStateName, {}) as any;
webViewState[request.key] = request.data;
this._context.globalState.update(MonitorView.GlobalStateName, webViewState);
return;
case 'OpenInNewWindow':
// Opening another WebView
this._childWebViewPanels.push(this.showWebView(request.url));
return;
case 'SaveAs':
// Just to be extra sure...
if (!MonitorView.looksLikeSvg(request.data)) {
vscode.window.showErrorMessage(`Invalid data format. Save failed.`);
return;
}
// Saving some file to local hard drive
vscode.window.showSaveDialog({ filters: { 'SVG Images': ['svg'] } }).then(filePath => {
if (!filePath || !filePath.fsPath) {
return;
}
fs.writeFile(filePath!.fsPath, request.data, err => {
if (!err) {
vscode.window.showInformationMessage(`Saved to ${filePath!.fsPath}`);
} else {
vscode.window.showErrorMessage(`Failed to save. ${err}`);
}
});
});
return;
case 'GotoFunctionCode':
const func = this._functionsAndProxies[request.url];
if (!!func && !!func.filePath) {
vscode.window.showTextDocument(vscode.Uri.file(func.filePath)).then(ed => {
const pos = ed.document.positionAt(!!func.pos ? func.pos : 0);
ed.selection = new vscode.Selection(pos, pos);
ed.revealRange(new vscode.Range(pos, pos));
});
}
return;
case 'VisualizeFunctionsAsAGraph':
const ws = vscode.workspace;
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'host.json'))) {
this._functionGraphList.visualizeProjectPath(ws.rootPath);
}
return;
}
// Intercepting request for Function Map
if (request.method === "GET" && request.url === '/function-map') {
if (!this._functionProjectPath) {
return;
}
const requestId = request.id;
this._functionGraphList.traverseFunctions(this._functionProjectPath).then(result => {
this._functionsAndProxies = {};
for (const name in result.functions) {
this._functionsAndProxies[name] = result.functions[name];
}
for (const name in result.proxies) {
this._functionsAndProxies['proxy.' + name] = result.proxies[name];
}
panel.webview.postMessage({
id: requestId, data: {
functions: result.functions,
proxies: result.proxies
}
});
}, err => {
// err might fail to serialize here, so passing err.message only
panel.webview.postMessage({ id: requestId, err: { message: err.message } });
});
return;
}
// Then it's just a propagated HTTP request
const requestId = request.id;
const headers: any = {};
headers[SharedConstants.NonceHeaderName] = this._backend.backendCommunicationNonce;
// Workaround for https://github.com/Azure/azure-functions-durable-extension/issues/1926
var hubName = this._hubName;
if (hubName === 'TestHubName' && request.method === 'POST' && request.url.match(/\/(orchestrations|restart)$/i)) {
// Turning task hub name into lower case, this allows to bypass function name validation
hubName = 'testhubname';
}
axios.request({
url: `${this._backend.backendUrl}/--${hubName}${request.url}`,
method: request.method,
data: request.data,
headers
}).then(response => {
panel.webview.postMessage({ id: requestId, data: response.data });
}, err => {
panel.webview.postMessage({ id: requestId, err: { message: err.message, response: { data: !err.response ? undefined : err.response.data } } });
});
}, undefined, this._context.subscriptions);
return panel;
}
// Embeds the current color theme
private embedThemeAndSettings(html: string): string {
const theme = [2, 3].includes((vscode.window as any).activeColorTheme.kind) ? 'dark' : 'light';
return html.replace('<script>var DfmClientConfig={}</script>',
`<script>var DfmClientConfig={'theme':'${theme}','showTimeAs':'${Settings().showTimeAs}'}</script>`);
}
// Embeds the orchestrationId in the HTML served
private embedOrchestrationIdAndState(html: string, orchestrationId: string, state: any): string {
return html.replace(
`<script>var OrchestrationIdFromVsCode="",StateFromVsCode={}</script>`,
`<script>var OrchestrationIdFromVsCode="${orchestrationId}",StateFromVsCode=${JSON.stringify(state)}</script>`
);
}
// Embeds the isFunctionGraphAvailable flag in the HTML served
private embedIsFunctionGraphAvailable(html: string, isFunctionGraphAvailable: boolean): string {
if (!isFunctionGraphAvailable) {
return html;
}
return html.replace(
`<script>var IsFunctionGraphAvailable=0</script>`,
`<script>var IsFunctionGraphAvailable=1</script>`
);
}
private askForInstanceId(): Promise<string> {
return new Promise<string>((resolve, reject) => {
var instanceId = '';
const instanceIdPick = vscode.window.createQuickPick();
instanceIdPick.onDidHide(() => instanceIdPick.dispose());
instanceIdPick.onDidChangeSelection(items => {
if (!!items && !!items.length) {
instanceId = items[0].label;
}
});
// Still allowing to type free text
instanceIdPick.onDidChangeValue(value => {
instanceId = value;
// Loading suggestions from backend
if (instanceId.length > 1) {
this.getInstanceIdSuggestions(instanceId).then(suggestions => {
instanceIdPick.items = suggestions.map(id => {
return { label: id };
});
});
} else {
instanceIdPick.items = [];
}
});
instanceIdPick.onDidAccept(() => {
if (!!instanceId) {
resolve(instanceId);
}
instanceIdPick.hide();
});
instanceIdPick.title = `(${this.taskHubFullTitle}) instanceId to go to:`;
instanceIdPick.show();
// If nothing is selected, leaving the promise unresolved, so nothing more happens
});
}
// Human-readable TaskHub title in form '[storage-account]/[task-hub]'
private get taskHubFullTitle(): string {
return `${ConnStringUtils.GetStorageName(this._backend.storageConnectionStrings)}/${this._hubName}`;
}
// Returns orchestration/entity instanceIds that start with prefix
private getInstanceIdSuggestions(prefix: string): Promise<string[]> {
const headers: any = {};
headers[SharedConstants.NonceHeaderName] = this._backend.backendCommunicationNonce;
return axios.get(`${this._backend.backendUrl}/--${this._hubName}/id-suggestions(prefix='${prefix}')`, { headers })
.then(response => {
return response.data as string[];
});
}
}

Просмотреть файл

@ -0,0 +1,418 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
import { ConnStringUtils } from "./ConnStringUtils";
import { MonitorView } from "./MonitorView";
import { BackendProcess, StorageConnectionSettings, CreateAuthHeadersForTableStorage } from './BackendProcess';
import { Settings } from './Settings';
import { FunctionGraphList } from './FunctionGraphList';
// Represents all MonitorViews created so far
export class MonitorViewList {
constructor(private _context: vscode.ExtensionContext,
private _functionGraphList: FunctionGraphList,
private _onViewStatusChanged: () => void,
private _log: (line: string) => void) {
}
isAnyMonitorViewVisible(): boolean {
return Object.keys(this._monitorViews).some(k => !!this._monitorViews[k] && this._monitorViews[k].isVisible);
}
isMonitorViewVisible(connSettings: StorageConnectionSettings): boolean {
const monitorView = this._monitorViews[connSettings.hashKey];
return !!monitorView && monitorView.isVisible;
}
// Creates a new MonitorView with provided connection settings
getOrCreateFromStorageConnectionSettings(connSettings: StorageConnectionSettings): MonitorView {
var monitorView = this._monitorViews[connSettings.hashKey];
if (!!monitorView) {
return monitorView;
}
monitorView = new MonitorView(this._context,
this.getOrAddBackend(connSettings),
connSettings.hubName,
this._functionGraphList,
this._onViewStatusChanged);
this._monitorViews[connSettings.hashKey] = monitorView;
return monitorView;
}
// Gets an existing (first in the list) MonitorView,
// or initializes a new one by asking user for connection settings
getOrAdd(alwaysCreateNew: boolean): Promise<MonitorView> {
const keys = Object.keys(this._monitorViews);
if (!alwaysCreateNew && keys.length > 0) {
return Promise.resolve(this._monitorViews[keys[0]]);
}
return new Promise<MonitorView>((resolve, reject) => {
this.askForStorageConnectionSettings().then(connSettings => {
const monitorView = this.getOrCreateFromStorageConnectionSettings(connSettings);
resolve(monitorView);
}, reject);
});
}
firstOrDefault(): MonitorView | null {
const keys = Object.keys(this._monitorViews);
if (keys.length <= 0) {
return null;
}
return this._monitorViews[keys[0]];
}
// Parses local project files and tries to infer connction settings from them
getStorageConnectionSettingsFromCurrentProject(defaultTaskHubName?: string): StorageConnectionSettings | null {
const hostJson = this.readHostJson();
if (hostJson.storageProviderType === 'mssql') {
const sqlConnectionString = this.getValueFromLocalSettings(hostJson.connectionStringName);
if (!sqlConnectionString) {
return null;
}
return new StorageConnectionSettings(
[sqlConnectionString],
'DurableFunctionsHub',
true);
}
var hubName: string | undefined = hostJson.hubName;
if (!hubName) {
hubName = defaultTaskHubName;
if (!hubName) {
return null;
}
}
const storageConnString = this.getValueFromLocalSettings('AzureWebJobsStorage');
if (!storageConnString) {
return null;
}
return new StorageConnectionSettings([ConnStringUtils.ExpandEmulatorShortcutIfNeeded(storageConnString)], hubName, true);
}
// Stops all backend processes and closes all views
cleanup(): Promise<any> {
Object.keys(this._monitorViews).map(k => this._monitorViews[k].cleanup());
this._monitorViews = {};
const backends = this._backends;
this._backends = {};
return Promise.all(Object.keys(backends).map(k => backends[k].cleanup()));
}
detachBackend(storageConnStrings: string[]): Promise<any> {
const connStringHashKey = StorageConnectionSettings.GetConnStringHashKey(storageConnStrings);
// Closing all views related to this connection
for (const key of Object.keys(this._monitorViews)) {
const monitorView = this._monitorViews[key];
if (monitorView.storageConnectionSettings.connStringHashKey === connStringHashKey) {
monitorView.cleanup();
delete this._monitorViews[key];
}
}
// Stopping background process
const backendProcess = this._backends[connStringHashKey];
if (!backendProcess) {
return Promise.resolve();
}
return backendProcess.cleanup().then(() => {
delete this._backends[connStringHashKey];
});
}
getBackendUrl(storageConnStrings: string[]): string {
const backendProcess = this._backends[StorageConnectionSettings.GetConnStringHashKey(storageConnStrings)];
return !backendProcess ? '' : backendProcess.backendUrl;
}
showUponDebugSession(connSettingsFromCurrentProject?: StorageConnectionSettings): Promise<MonitorView> {
if (!connSettingsFromCurrentProject) {
return this.getOrAdd(true);
}
return Promise.resolve(this.getOrCreateFromStorageConnectionSettings(connSettingsFromCurrentProject));
}
private _monitorViews: { [key: string]: MonitorView } = {};
private _backends: { [key: string]: BackendProcess } = {};
private getOrAddBackend(connSettings: StorageConnectionSettings): BackendProcess {
// If a backend for this connection already exists, then just returning the existing one.
var backendProcess = this._backends[connSettings.connStringHashKey];
if (!backendProcess) {
var binariesFolder = Settings().customPathToBackendBinaries;
if (!binariesFolder) {
if (connSettings.isMsSql) {
binariesFolder = path.join(this._context.extensionPath, 'custom-backends', 'mssql');
} else if (Settings().backendVersionToUse === '.Net Core 2.1') {
binariesFolder = path.join(this._context.extensionPath, 'custom-backends', 'netcore21');
} else if (Settings().backendVersionToUse === '.Net Core 3.1') {
binariesFolder = path.join(this._context.extensionPath, 'custom-backends', 'netcore31');
} else {
binariesFolder = path.join(this._context.extensionPath, 'backend');
}
}
backendProcess = new BackendProcess(
binariesFolder,
connSettings,
() => this.detachBackend(connSettings.storageConnStrings),
this._log
);
this._backends[connSettings.connStringHashKey] = backendProcess;
}
return backendProcess;
}
// Obtains Storage Connection String and Hub Name from user
private askForStorageConnectionSettings(): Promise<StorageConnectionSettings> {
return new Promise<StorageConnectionSettings>((resolve, reject) => {
// Asking the user for Connection String
var connStringToShow = '';
const connStringFromLocalSettings = this.getValueFromLocalSettings('AzureWebJobsStorage');
if (!!connStringFromLocalSettings) {
connStringToShow = StorageConnectionSettings.MaskStorageConnString(connStringFromLocalSettings);
}
vscode.window.showInputBox({ value: connStringToShow, prompt: 'Storage or MSSQL Connection String' }).then(connString => {
if (!connString) {
// Leaving the promise unresolved, so nothing more happens
return;
}
// If the user didn't change it
if (connString === connStringToShow) {
// Then setting it back to non-masked one
connString = connStringFromLocalSettings;
}
// If it is MSSQL storage provider
if (!!ConnStringUtils.GetSqlServerName(connString)) {
resolve(new StorageConnectionSettings([connString!], 'DurableFunctionsHub'));
return;
}
// Dealing with 'UseDevelopmentStorage=true' early
connString = ConnStringUtils.ExpandEmulatorShortcutIfNeeded(connString);
// Asking the user for Hub Name
var hubName = '';
const hubPick = vscode.window.createQuickPick();
hubPick.onDidHide(() => hubPick.dispose());
hubPick.onDidChangeSelection(items => {
if (!!items && !!items.length) {
hubName = items[0].label;
}
});
// Still allowing to type free text
hubPick.onDidChangeValue(value => {
hubName = value;
});
hubPick.onDidAccept(() => {
if (!!hubName) {
resolve(new StorageConnectionSettings([connString!], hubName));
}
hubPick.hide();
});
hubPick.title = 'Hub Name';
var hubNameFromHostJson = this.readHostJson().hubName;
if (!!hubNameFromHostJson) {
hubPick.items = [{
label: hubNameFromHostJson,
description: '(from host.json)'
}];
hubPick.placeholder = hubNameFromHostJson;
} else {
hubPick.items = [{
label: 'DurableFunctionsHub',
description: '(default hub name)'
}];
hubPick.placeholder = 'DurableFunctionsHub';
}
// Loading other hub names directly from Table Storage
this.loadHubNamesFromTableStorage(connString).then(hubNames => {
if (hubNames.length >= 0) {
// Adding loaded names to the list
hubPick.items = hubNames.map(label => {
return { label: label, description: '(from Table Storage)' };
});
hubPick.placeholder = hubNames[0];
}
});
hubPick.show();
// If nothing is selected, leaving the promise unresolved, so nothing more happens
}, reject);
});
}
private loadHubNamesFromTableStorage(storageConnString: string): Promise<string[]> {
return new Promise<string[]>((resolve) => {
const accountName = ConnStringUtils.GetAccountName(storageConnString);
const accountKey = ConnStringUtils.GetAccountKey(storageConnString);
const tableEndpoint = ConnStringUtils.GetTableEndpoint(storageConnString);
if (!accountName || !accountKey) {
// Leaving the promise unresolved
return;
}
getTaskHubNamesFromTableStorage(accountName, accountKey, tableEndpoint).then(hubNames => {
if (!hubNames || hubNames.length <= 0) {
// Leaving the promise unresolved
return;
}
resolve(hubNames);
}, err => {
console.log(`Failed to load the list of tables. ${err.message}`);
// Leaving the promise unresolved
});
});
}
private getValueFromLocalSettings(valueName: string): string {
const ws = vscode.workspace;
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'local.settings.json'))) {
const localSettings = JSON.parse(fs.readFileSync(path.join(ws.rootPath, 'local.settings.json'), 'utf8'));
if (!!localSettings.Values && !!localSettings.Values[valueName]) {
return localSettings.Values[valueName];
}
}
return '';
}
private readHostJson(): { hubName: string, storageProviderType: 'default' | 'mssql', connectionStringName: string } {
const result = { hubName: '', storageProviderType: 'default' as any, connectionStringName: '' };
const ws = vscode.workspace;
if (!!ws.rootPath && fs.existsSync(path.join(ws.rootPath, 'host.json'))) {
var hostJson;
try {
hostJson = JSON.parse(fs.readFileSync(path.join(ws.rootPath, 'host.json'), 'utf8'));
} catch (err) {
console.log(`Failed to parse host.json. ${(err as any).message}`);
return result;
}
if (!!hostJson && !!hostJson.extensions && hostJson.extensions.durableTask) {
const durableTask = hostJson.extensions.durableTask;
if (!!durableTask.HubName || !!durableTask.hubName) {
result.hubName = !!durableTask.HubName ? durableTask.HubName : durableTask.hubName
}
if (!!durableTask.storageProvider && durableTask.storageProvider.type === 'mssql') {
result.storageProviderType = 'mssql';
result.connectionStringName = durableTask.storageProvider.connectionStringName;
}
}
}
return result;
}
}
// Tries to load the list of TaskHub names from a storage account.
// Had to handcraft this code, since @azure/data-tables package is still in beta :(
export async function getTaskHubNamesFromTableStorage(accountName: string, accountKey: string, tableEndpointUrl: string): Promise<string[] | null> {
if (!tableEndpointUrl) {
tableEndpointUrl = `https://${accountName}.table.core.windows.net/`;
} else if (!tableEndpointUrl.endsWith('/')) {
tableEndpointUrl += '/';
}
// Local emulator URLs contain account name _after_ host (like http://127.0.0.1:10002/devstoreaccount1/ ),
// and this part should be included when obtaining SAS
const tableEndpointUrlParts = tableEndpointUrl.split('/');
const tableQueryUrl = (tableEndpointUrlParts.length > 3 && !!tableEndpointUrlParts[3]) ?
`${tableEndpointUrlParts[3]}/Tables` :
'Tables';
// Creating the SharedKeyLite signature to query Table Storage REST API for the list of tables
const authHeaders = CreateAuthHeadersForTableStorage(accountName, accountKey, tableQueryUrl);
var response: any;
try {
response = await axios.get(`${tableEndpointUrl}Tables`, { headers: authHeaders });
} catch (err) {
console.log(`Failed to load hub names from table storage. ${(err as any).message}`);
}
if (!response || !response.data || !response.data.value || response.data.value.length <= 0) {
return null;
}
const instancesTables: string[] = response.data.value.map((table: any) => table.TableName)
.filter((tableName: string) => tableName.endsWith('Instances'))
.map((tableName: string) => tableName.substr(0, tableName.length - 'Instances'.length));
const historyTables: string[] = response.data.value.map((table: any) => table.TableName)
.filter((tableName: string) => tableName.endsWith('History'))
.map((tableName: string) => tableName.substr(0, tableName.length - 'History'.length));
// Considering it to be a hub, if it has both *Instances and *History tables
return instancesTables.filter(name => historyTables.indexOf(name) >= 0);
}

Просмотреть файл

@ -0,0 +1,38 @@
import * as vscode from 'vscode';
// Returns config values stored in VsCode's settings.json
export function Settings(): ISettings {
const config = vscode.workspace.getConfiguration('durableFunctionsMonitor');
// Better to have default values hardcoded here (not only in package.json) as well
return {
backendBaseUrl: config.get<string>('backendBaseUrl', 'http://localhost:{portNr}/a/p/i'),
backendVersionToUse: config.get<'Default' | '.Net Core 3.1'>('backendVersionToUse', 'Default'),
customPathToBackendBinaries: config.get<string>('customPathToBackendBinaries', ''),
backendTimeoutInSeconds: config.get<number>('backendTimeoutInSeconds', 60),
storageEmulatorConnectionString: config.get<string>('storageEmulatorConnectionString', 'AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;'),
enableLogging: config.get<boolean>('enableLogging', false),
showTimeAs: config.get<'UTC' | 'Local'>('showTimeAs', 'UTC'),
showWhenDebugSessionStarts: config.get<boolean>('showWhenDebugSessionStarts', false),
};
}
// Updates a config value stored in VsCode's settings.json
export function UpdateSetting(name: string, val: any) {
const config = vscode.workspace.getConfiguration('durableFunctionsMonitor');
config.update(name, val, true);
}
interface ISettings
{
backendBaseUrl: string;
backendVersionToUse: 'Default' | '.Net Core 3.1' | '.Net Core 2.1';
customPathToBackendBinaries: string;
backendTimeoutInSeconds: number;
storageEmulatorConnectionString: string;
enableLogging: boolean;
showTimeAs: 'UTC' | 'Local';
showWhenDebugSessionStarts: boolean;
}

Просмотреть файл

@ -0,0 +1,4 @@
export const NonceEnvironmentVariableName = 'DFM_NONCE';
export const NonceHeaderName = 'x-dfm-nonce';
export const MsSqlConnStringEnvironmentVariableName = 'DFM_SQL_CONNECTION_STRING';
export const HubNameEnvironmentVariableName = 'DFM_HUB_NAME';

Просмотреть файл

@ -0,0 +1,105 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { StorageConnectionSettings } from './BackendProcess';
import { ConnStringUtils } from "./ConnStringUtils";
import { TaskHubTreeItem } from "./TaskHubTreeItem";
import { MonitorViewList } from "./MonitorViewList";
// Represents the Storage Account item in the TreeView
export class StorageAccountTreeItem extends vscode.TreeItem {
constructor(private _connStrings: string[],
private _resourcesFolderPath: string,
private _monitorViewList: MonitorViewList,
private _fromLocalSettingsJson: boolean = false) {
super(ConnStringUtils.GetStorageName(_connStrings), vscode.TreeItemCollapsibleState.Expanded);
this.isMsSqlStorage = !!ConnStringUtils.GetSqlServerName(this._connStrings[0]);
}
readonly isMsSqlStorage: boolean;
isV2StorageAccount: boolean = false;
storageAccountId: string = '';
get isAttached(): boolean {
return !!this.backendUrl;
}
get backendUrl(): string {
return this._monitorViewList.getBackendUrl(this._connStrings);
}
get storageName(): string {
return this.label!;
}
get storageConnStrings(): string[] {
return this._connStrings;
}
get childItems(): TaskHubTreeItem[] {
return this._taskHubItems;
}
get tooltip(): string {
if (this._fromLocalSettingsJson) {
return `from local.settings.json`;
}
if (!!this.isMsSqlStorage) {
return 'MSSQL Storage Provider';
}
return StorageConnectionSettings.MaskStorageConnString(this._connStrings[0]);
}
// Something to show to the right of this item
get description(): string {
return `${this._taskHubItems.length} Task Hubs`;
}
// Item's icon
get iconPath(): string {
if (!!this.isMsSqlStorage) {
return path.join(this._resourcesFolderPath, this.isAttached ? 'mssqlAttached.svg' : 'mssql.svg');
}
if (this.isV2StorageAccount) {
return path.join(this._resourcesFolderPath, this.isAttached ? 'storageAccountV2Attached.svg' : 'storageAccountV2.svg');
}
return path.join(this._resourcesFolderPath, this.isAttached ? 'storageAccountAttached.svg' : 'storageAccount.svg');
}
// For binding context menu to this tree node
get contextValue(): string {
return this.isAttached ? 'storageAccount-attached' : 'storageAccount-detached';
}
// For sorting
static compare(first: StorageAccountTreeItem, second: StorageAccountTreeItem): number {
const a = first.storageName.toLowerCase();
const b = second.storageName.toLowerCase();
return a === b ? 0 : (a < b ? -1 : 1);
}
// Creates or returns existing TaskHubTreeItem by hub name
getOrAdd(hubName: string): TaskHubTreeItem {
var hubItem = this._taskHubItems.find(taskHub => taskHub.hubName.toLowerCase() === hubName.toLowerCase());
if (!hubItem) {
hubItem = new TaskHubTreeItem(this, hubName, this._resourcesFolderPath);
this._taskHubItems.push(hubItem);
this._taskHubItems.sort(TaskHubTreeItem.compare);
}
return hubItem;
}
isTaskHubVisible(hubName: string): boolean {
return this._monitorViewList.isMonitorViewVisible(new StorageConnectionSettings(this._connStrings, hubName));
}
private _taskHubItems: TaskHubTreeItem[] = [];
}

Просмотреть файл

@ -0,0 +1,74 @@
import { MonitorView } from "./MonitorView";
import { MonitorViewList } from "./MonitorViewList";
import { StorageAccountTreeItem } from "./StorageAccountTreeItem";
import { StorageConnectionSettings } from "./BackendProcess";
import { ConnStringUtils } from "./ConnStringUtils";
import { TaskHubTreeItem } from "./TaskHubTreeItem";
// Represents the list of Storage Account items in the TreeView
export class StorageAccountTreeItems {
constructor(private _resourcesFolderPath: string, private _monitorViewList: MonitorViewList) {}
get nodes(): StorageAccountTreeItem[] {
return this._storageAccountItems;
}
get taskHubNodes(): TaskHubTreeItem[] {
return ([] as TaskHubTreeItem[]).concat(...this._storageAccountItems.map(n => n.childItems));
}
// Adds a node to the tree for MonitorView, that's already running
addNodeForMonitorView(monitorView: MonitorView): void {
const storageConnStrings = monitorView.storageConnectionSettings.storageConnStrings;
const storageName = ConnStringUtils.GetStorageName(storageConnStrings);
const hubName = monitorView.storageConnectionSettings.hubName;
// Only creating a new tree node, if no node for this account exists so far
var node = this._storageAccountItems.find(item => item.storageName.toLowerCase() === storageName.toLowerCase());
if (!node) {
node = new StorageAccountTreeItem(storageConnStrings, this._resourcesFolderPath, this._monitorViewList);
this._storageAccountItems.push(node);
this._storageAccountItems.sort(StorageAccountTreeItem.compare);
}
node.getOrAdd(hubName);
}
// Adds a detached node to the tree for the specified storage connection settings
addNodeForConnectionSettings(connSettings: StorageConnectionSettings, isV2StorageAccount: boolean = false, storageAccountId: string = ''): void {
const storageConnStrings = connSettings.storageConnStrings;
const hubName = connSettings.hubName;
// Trying to infer account name from connection string
const storageName = ConnStringUtils.GetStorageName(storageConnStrings);
if (!storageName) {
return;
}
// Only creating a new tree node, if no node for this account exists so far
var node = this._storageAccountItems.find(item => item.storageName === storageName);
if (!node) {
node = new StorageAccountTreeItem(storageConnStrings,
this._resourcesFolderPath,
this._monitorViewList,
connSettings.isFromLocalSettingsJson
);
this._storageAccountItems.push(node);
this._storageAccountItems.sort(StorageAccountTreeItem.compare);
}
node.isV2StorageAccount = isV2StorageAccount;
node.storageAccountId = storageAccountId;
node.getOrAdd(hubName);
}
private _storageAccountItems: StorageAccountTreeItem[] = [];
}

Просмотреть файл

@ -0,0 +1,53 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { StorageAccountTreeItem } from "./StorageAccountTreeItem";
import { StorageAccountTreeItems } from "./StorageAccountTreeItems";
// Represents an Azure Subscription in the TreeView
export class SubscriptionTreeItem extends vscode.TreeItem {
get isSubscriptionTreeItem(): boolean { return true; }
// Returns storage account nodes, that belong to this subscription
get storageAccountNodes(): StorageAccountTreeItem[] {
return this._storageAccounts.nodes.filter(a => this.isMyStorageAccount(a));
}
constructor(subscriptionName: string,
private _storageAccounts: StorageAccountTreeItems,
private _storageAccountNames: string[],
protected _resourcesFolderPath: string
) {
super(subscriptionName, vscode.TreeItemCollapsibleState.Expanded);
this.iconPath = path.join(this._resourcesFolderPath, 'azureSubscription.svg');
}
// Checks whether this storage account belongs to this subscription.
isMyStorageAccount(accNode: StorageAccountTreeItem): boolean {
// The only way to do this is by matching the account name against all account names in this subscription.
// We need to fetch these acccount names for other purposes anyway, so why not using them here as well
// (as opposite to making separate roundtrips to get subscriptionId for a given account).
return this._storageAccountNames.includes(accNode.storageName);
}
}
// Represents a special node in the TreeView where all 'orphaned' (those with unidentified subscription) storage accounts go
export class DefaultSubscriptionTreeItem extends SubscriptionTreeItem {
constructor(storageAccounts: StorageAccountTreeItems,
private _otherSubscriptionNodes: SubscriptionTreeItem[],
resourcesFolderPath: string
) {
super('Storages', storageAccounts, [], resourcesFolderPath);
this.iconPath = path.join(this._resourcesFolderPath, 'storageAccounts.svg');
}
// Checks whether this storage account belongs to this tree item.
isMyStorageAccount(accNode: StorageAccountTreeItem): boolean {
// Let's see if this account belongs to any other subscription node. If not - it's ours.
return this._otherSubscriptionNodes.every(n => !n.isMyStorageAccount(accNode));
}
}

Просмотреть файл

@ -0,0 +1,239 @@
import * as vscode from 'vscode';
import { StorageManagementClient } from "@azure/arm-storage";
import { StorageAccount } from "@azure/arm-storage/src/models";
import { SubscriptionTreeItem, DefaultSubscriptionTreeItem } from "./SubscriptionTreeItem";
import { StorageAccountTreeItems } from "./StorageAccountTreeItems";
import { getTaskHubNamesFromTableStorage } from './MonitorViewList';
import { ConnStringUtils } from "./ConnStringUtils";
import { Settings } from './Settings';
import { StorageConnectionSettings } from "./BackendProcess";
// Full typings for this can be found here: https://github.com/microsoft/vscode-azure-account/blob/master/src/azure-account.api.d.ts
type AzureSubscription = { session: { credentials2: any }, subscription: { subscriptionId: string, displayName: string } };
// Represents the list of Azure Subscriptions in the TreeView
export class SubscriptionTreeItems {
constructor(private _context: vscode.ExtensionContext,
private _azureAccount: any,
private _storageAccounts: StorageAccountTreeItems,
private _onStorageAccountsChanged: () => void,
private _resourcesFolderPath: string,
private _log: (l: string) => void)
{ }
// Returns subscription nodes, but only those that have some TaskHubs in them
async getNonEmptyNodes(): Promise<SubscriptionTreeItem[]> {
if (!this._nodes) {
// Need to wait until Azure Account ext loads the filtered list of subscriptions
if (!this._azureAccount || !await this._azureAccount.waitForFilters()) {
this._nodes = [];
} else {
// Showing only filtered subscriptions and ignoring those, which are hidden
const subscriptions = this._azureAccount.filters;
this._nodes = await this.loadSubscriptionNodes(subscriptions);
}
// Adding the 'default subscription' node, where all orphaned (unrecognized) storage accounts will go to.
this._nodes.push(new DefaultSubscriptionTreeItem(this._storageAccounts, this._nodes.slice(), this._resourcesFolderPath));
// Also pinging local Storage Emulator and deliberately not awaiting
this.tryLoadingTaskHubsForLocalStorageEmulator();
}
// Only showing non-empty subscriptions
return this._nodes.filter(n => n.storageAccountNodes.length > 0);
}
cleanup(): void {
this._nodes = undefined;
}
private _nodes?: SubscriptionTreeItem[];
private async tryLoadingStorageAccountsForSubscription(storageManagementClient: StorageManagementClient): Promise<StorageAccount[]> {
const result: StorageAccount[] = [];
var storageAccountsPartialResponse = await storageManagementClient.storageAccounts.list();
result.push(...storageAccountsPartialResponse);
while (!!storageAccountsPartialResponse.nextLink) {
storageAccountsPartialResponse = await storageManagementClient.storageAccounts.listNext(storageAccountsPartialResponse.nextLink);
result.push(...storageAccountsPartialResponse);
}
return result;
}
private static HasAlreadyShownStorageV2Warning = false;
private showWarning4V2StorageAccounts(v2AccountNames: string[]): void {
const DfmDoNotShowStorageV2Warning = 'DfmDoNotShowStorageV2Warning';
if (!!SubscriptionTreeItems.HasAlreadyShownStorageV2Warning || !!this._context.globalState.get(DfmDoNotShowStorageV2Warning, false) || !v2AccountNames.length) {
return;
}
SubscriptionTreeItems.HasAlreadyShownStorageV2Warning = true;
const prompt = `Looks like your Durable Functions are using the following General-purpose V2 Storage accounts: ${v2AccountNames.join(', ')}. Combined with Durable Functions, V2 Storage accounts can be more expensive under high loads. Consider using General-purpose V1 Storage instead.`;
vscode.window.showWarningMessage(prompt, 'OK', `Don't Show Again`).then(answer => {
if (answer === `Don't Show Again`) {
this._context.globalState.update(DfmDoNotShowStorageV2Warning, true);
}
});
}
private async tryLoadingTaskHubsForSubscription(storageManagementClient: StorageManagementClient, storageAccounts: StorageAccount[]): Promise<boolean> {
const v2AccountNames: string[] = [];
var taskHubsAdded = false;
await Promise.all(storageAccounts.map(async storageAccount => {
// Extracting resource group name
const match = /\/resourceGroups\/([^\/]+)\/providers/gi.exec(storageAccount.id!);
if (!match || match.length <= 0) {
return;
}
const resourceGroupName = match[1];
const storageKeys = await storageManagementClient.storageAccounts.listKeys(resourceGroupName, storageAccount.name!);
if (!storageKeys.keys || storageKeys.keys.length <= 0) {
return;
}
// Choosing the key that looks best
var storageKey = storageKeys.keys.find(k => !k.permissions || k.permissions.toLowerCase() === "full");
if (!storageKey) {
storageKey = storageKeys.keys.find(k => !k.permissions || k.permissions.toLowerCase() === "read");
}
if (!storageKey) {
return;
}
var tableEndpoint = '';
if (!!storageAccount.primaryEndpoints) {
tableEndpoint = storageAccount.primaryEndpoints.table!;
}
const hubNames = await getTaskHubNamesFromTableStorage(storageAccount.name!, storageKey.value!, tableEndpoint);
if (!hubNames || !hubNames.length) {
return;
}
const isV2StorageAccount = storageAccount.kind === 'StorageV2';
if (isV2StorageAccount) {
v2AccountNames.push(storageAccount.name!);
}
for (const hubName of hubNames) {
this._storageAccounts.addNodeForConnectionSettings(
new StorageConnectionSettings([this.getConnectionStringForStorageAccount(storageAccount, storageKey.value!)], hubName, false),
isV2StorageAccount,
storageAccount.id
);
taskHubsAdded = true;
}
}));
// Notifying about potentially higher costs of V2 accounts
this.showWarning4V2StorageAccounts(v2AccountNames);
return taskHubsAdded;
}
private async tryLoadingTaskHubsForLocalStorageEmulator(): Promise<void> {
const emulatorConnString = Settings().storageEmulatorConnectionString;
const accountName = ConnStringUtils.GetAccountName(emulatorConnString);
const accountKey = ConnStringUtils.GetAccountKey(emulatorConnString);
const tableEndpoint = ConnStringUtils.GetTableEndpoint(emulatorConnString);
const hubNames = await getTaskHubNamesFromTableStorage(accountName, accountKey, tableEndpoint);
if (!hubNames) {
return;
}
for (const hubName of hubNames) {
this._storageAccounts.addNodeForConnectionSettings(new StorageConnectionSettings([emulatorConnString], hubName));
}
if (hubNames.length > 0) {
this._onStorageAccountsChanged();
}
}
private getConnectionStringForStorageAccount(account: StorageAccount, storageKey: string): string {
var endpoints = '';
if (!!account.primaryEndpoints) {
endpoints = `BlobEndpoint=${account.primaryEndpoints!.blob};QueueEndpoint=${account.primaryEndpoints!.queue};TableEndpoint=${account.primaryEndpoints!.table};FileEndpoint=${account.primaryEndpoints!.file};`;
} else {
endpoints = `BlobEndpoint=https://${account.name}.blob.core.windows.net/;QueueEndpoint=https://${account.name}.queue.core.windows.net/;TableEndpoint=https://${account.name}.table.core.windows.net/;FileEndpoint=https://${account.name}.file.core.windows.net/;`;
}
return `DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${storageKey};${endpoints}`;
}
private async loadSubscriptionNodes(subscriptions: AzureSubscription[]): Promise<SubscriptionTreeItem[]> {
const results = await Promise.all(subscriptions.map(async s => {
try {
const storageManagementClient = new StorageManagementClient(s.session.credentials2, s.subscription.subscriptionId);
// Trying to load all storage account names in this subscription.
// We need that list of names to subsequently match storage account nodes with their subscription nodes.
const storageAccounts = await this.tryLoadingStorageAccountsForSubscription(storageManagementClient);
// Now let's try to detect and load TaskHubs in this subscription.
// Many things can go wrong there, that is why we're doing it so asynchronously
this.tryLoadingTaskHubsForSubscription(storageManagementClient, storageAccounts)
.then(anyMoreTaskHubsAdded => {
if (anyMoreTaskHubsAdded) {
this._onStorageAccountsChanged();
}
}, err => {
this._onStorageAccountsChanged();
this._log(`Failed to load TaskHubs from subscription ${s.subscription.displayName}. ${err.message}`);
});
return {
subscriptionId: s.subscription.subscriptionId,
subscriptionName: s.subscription.displayName,
storageAccountNames: storageAccounts.map(a => a.name!)
};
} catch (err) {
this._log(`Failed to load storage accounts from subscription ${s.subscription.displayName}. ${err.message}`);
return null;
}
}));
return results
.filter(r => r !== null)
.map(r => new SubscriptionTreeItem(
r!.subscriptionName,
this._storageAccounts,
r!.storageAccountNames,
this._resourcesFolderPath
));
}
}

Просмотреть файл

@ -0,0 +1,74 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { StorageConnectionSettings } from './BackendProcess';
import { StorageAccountTreeItem } from "./StorageAccountTreeItem";
// Represents the Task Hub item in the TreeView
export class TaskHubTreeItem extends vscode.TreeItem {
constructor(private _parentItem: StorageAccountTreeItem, private _hubName: string, private _resourcesFolderPath: string) {
super(_hubName);
}
get storageAccountId(): string {
return this._parentItem.storageAccountId;
}
get subscriptionId(): string {
const match = /\/subscriptions\/([^\/]+)\/resourceGroups/gi.exec(this._parentItem.storageAccountId);
if (!match || match.length <= 0) {
return '';
}
return match[1];
}
get hubName(): string {
return this._hubName;
}
// Gets associated storage connection settings
get storageConnectionSettings(): StorageConnectionSettings {
return new StorageConnectionSettings(this._parentItem.storageConnStrings, this._hubName);
}
// Item's icon
get iconPath(): string {
return path.join(this._resourcesFolderPath, this._parentItem.isTaskHubVisible(this._hubName) ? 'taskHubAttached.svg' : 'taskHub.svg');
}
// As a tooltip, showing the backend's URL
get tooltip(): string {
const backendUrl = this._parentItem.backendUrl;
return !backendUrl ? '' : `${backendUrl}/${this._hubName}`;
}
// This is what happens when the item is being clicked
get command(): vscode.Command {
return {
title: 'Attach',
command: 'durableFunctionsMonitorTreeView.attachToTaskHub',
arguments: [this]
};
}
// For binding context menu to this tree node
get contextValue(): string {
return this._parentItem.isAttached ? 'taskHub-attached' : 'taskHub-detached';
}
// For sorting
static compare(first: TaskHubTreeItem, second: TaskHubTreeItem): number {
const a = first.label!.toLowerCase();
const b = second.label!.toLowerCase();
return a === b ? 0 : (a < b ? -1 : 1);
}
// Drops itself from parent's list
removeFromTree(): void {
this._parentItem.childItems.splice(this._parentItem.childItems.indexOf(this), 1);
}
}

Просмотреть файл

@ -0,0 +1,35 @@
export type FunctionsMap = {
[name: string]: {
bindings: any[],
isCalledBy: string[],
isSignalledBy: { name: string, signalName: string }[],
isCalledByItself?: boolean,
filePath?: string,
pos?: number,
lineNr?: number
}
};
export type ProxiesMap = {
[name: string]: {
matchCondition?: {
methods?: string[];
route?: string;
};
backendUri?: string;
requestOverrides?: {};
responseOverrides?: {};
filePath?: string,
pos?: number,
lineNr?: number,
warningNotAddedToCsProjFile?: boolean
}
};
export type TraverseFunctionResult = {
functions: FunctionsMap;
proxies: ProxiesMap;
tempFolders: string[];
projectFolder: string;
};

Просмотреть файл

@ -0,0 +1,347 @@
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { FunctionsMap, ProxiesMap, TraverseFunctionResult } from './FunctionsMap';
import {
getCodeInBrackets, TraversalRegexes, DotNetBindingsParser,
isDotNetProjectAsync, posToLineNr, cloneFromGitHub
} from './traverseFunctionProjectUtils';
const ExcludedFolders = ['node_modules', 'obj', '.vs', '.vscode', '.env', '.python_packages', '.git', '.github'];
// Collects all function.json files in a Functions project. Also tries to supplement them with bindings
// extracted from .Net code (if the project is .Net). Also parses and organizes orchestrators/activities
// (if the project uses Durable Functions)
export async function traverseFunctionProject(projectFolder: string, log: (s: any) => void)
: Promise<TraverseFunctionResult> {
var functions: FunctionsMap = {}, tempFolders = [];
// If it is a git repo, cloning it
if (projectFolder.toLowerCase().startsWith('http')) {
log(`Cloning ${projectFolder}`);
const gitInfo = await cloneFromGitHub(projectFolder);
log(`Successfully cloned to ${gitInfo.gitTempFolder}`);
tempFolders.push(gitInfo.gitTempFolder);
projectFolder = gitInfo.projectFolder;
}
const hostJsonMatch = await findFileRecursivelyAsync(projectFolder, 'host.json', false);
if (!hostJsonMatch) {
throw new Error('host.json file not found under the provided project path');
}
log(`>>> Found host.json at ${hostJsonMatch.filePath}`);
var hostJsonFolder = path.dirname(hostJsonMatch.filePath);
// If it is a C# function, we'll need to dotnet publish first
if (await isDotNetProjectAsync(hostJsonFolder)) {
const publishTempFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'dotnet-publish-'));
tempFolders.push(publishTempFolder);
log(`>>> Publishing ${hostJsonFolder} to ${publishTempFolder}...`);
execSync(`dotnet publish -o ${publishTempFolder}`, { cwd: hostJsonFolder });
hostJsonFolder = publishTempFolder;
}
// Reading function.json files, in parallel
const promises = (await fs.promises.readdir(hostJsonFolder)).map(async functionName => {
const fullPath = path.join(hostJsonFolder, functionName);
const functionJsonFilePath = path.join(fullPath, 'function.json');
const isDirectory = (await fs.promises.lstat(fullPath)).isDirectory();
const functionJsonExists = fs.existsSync(functionJsonFilePath);
if (isDirectory && functionJsonExists) {
try {
const functionJsonString = await fs.promises.readFile(functionJsonFilePath, { encoding: 'utf8' });
const functionJson = JSON.parse(functionJsonString);
functions[functionName] = { bindings: functionJson.bindings, isCalledBy: [], isSignalledBy: [] };
} catch (err) {
log(`>>> Failed to parse ${functionJsonFilePath}: ${err}`);
}
}
});
await Promise.all(promises);
// Now enriching data from function.json with more info extracted from code
functions = await mapOrchestratorsAndActivitiesAsync(functions, projectFolder, hostJsonFolder);
// Also reading proxies
const proxies = await readProxiesJson(projectFolder, log);
return { functions, proxies, tempFolders, projectFolder };
}
// Tries to read proxies.json file from project folder
async function readProxiesJson(projectFolder: string, log: (s: any) => void): Promise<ProxiesMap> {
const proxiesJsonPath = path.join(projectFolder, 'proxies.json');
if (!fs.existsSync(proxiesJsonPath)) {
return {};
}
const proxiesJsonString = await fs.promises.readFile(proxiesJsonPath, { encoding: 'utf8' });
try {
const proxies = JSON.parse(proxiesJsonString).proxies as ProxiesMap;
if (!proxies) {
return {};
}
var notAddedToCsProjFile = false;
if (await isDotNetProjectAsync(projectFolder)) {
// Also checking that proxies.json is added to .csproj file
const csProjFile = await findFileRecursivelyAsync(projectFolder, '.+\\.csproj$', true);
const proxiesJsonEntryRegex = new RegExp(`\\s*=\\s*"proxies.json"\\s*>`);
if (!!csProjFile && csProjFile.code && (!proxiesJsonEntryRegex.exec(csProjFile.code))) {
notAddedToCsProjFile = true;
}
}
// Also adding filePath and lineNr
for (var proxyName in proxies) {
const proxy = proxies[proxyName];
proxy.filePath = proxiesJsonPath;
if (notAddedToCsProjFile) {
proxy.warningNotAddedToCsProjFile = true;
}
const proxyNameRegex = new RegExp(`"${proxyName}"\\s*:`);
const match = proxyNameRegex.exec(proxiesJsonString);
if (!!match) {
proxy.pos = match.index;
proxy.lineNr = posToLineNr(proxiesJsonString, proxy.pos);
}
}
return proxies;
} catch(err) {
log(`>>> Failed to parse ${proxiesJsonPath}: ${err}`);
return {};
}
}
// fileName can be a regex, pattern should be a regex (which will be searched for in the matching files).
// If returnFileContents == true, returns file content. Otherwise returns full path to the file.
async function findFileRecursivelyAsync(folder: string, fileName: string, returnFileContents: boolean, pattern?: RegExp)
: Promise<{ filePath: string, code?: string, pos?: number, length?: number } | undefined> {
const fileNameRegex = new RegExp(fileName, 'i');
for (const name of await fs.promises.readdir(folder)) {
var fullPath = path.join(folder, name);
if ((await fs.promises.lstat(fullPath)).isDirectory()) {
if (ExcludedFolders.includes(name.toLowerCase())) {
continue;
}
const result = await findFileRecursivelyAsync(fullPath, fileName, returnFileContents, pattern);
if (!!result) {
return result;
}
} else if (!!fileNameRegex.exec(name)) {
if (!pattern) {
return {
filePath: fullPath,
code: returnFileContents ? (await fs.promises.readFile(fullPath, { encoding: 'utf8' })) : undefined
};
}
const code = await fs.promises.readFile(fullPath, { encoding: 'utf8' });
const match = pattern.exec(code);
if (!!match) {
return {
filePath: fullPath,
code: returnFileContents ? code : undefined,
pos: match.index,
length: match[0].length
};
}
}
}
return undefined;
}
// Tries to match orchestrations and their activities by parsing source code
async function mapOrchestratorsAndActivitiesAsync(functions: FunctionsMap, projectFolder: string, hostJsonFolder: string): Promise<{}> {
const isDotNet = await isDotNetProjectAsync(projectFolder);
const functionNames = Object.keys(functions);
const orchestratorNames = functionNames.filter(name => functions[name].bindings.some((b: any) => b.type === 'orchestrationTrigger'));
const orchestrators = await getFunctionsAndTheirCodesAsync(orchestratorNames, isDotNet, projectFolder, hostJsonFolder);
const activityNames = Object.keys(functions).filter(name => functions[name].bindings.some((b: any) => b.type === 'activityTrigger'));
const activities = await getFunctionsAndTheirCodesAsync(activityNames, isDotNet, projectFolder, hostJsonFolder);
const entityNames = functionNames.filter(name => functions[name].bindings.some((b: any) => b.type === 'entityTrigger'));
const entities = await getFunctionsAndTheirCodesAsync(entityNames, isDotNet, projectFolder, hostJsonFolder);
const otherFunctionNames = functionNames.filter(name => !functions[name].bindings.some((b: any) => ['orchestrationTrigger', 'activityTrigger', 'entityTrigger'].includes(b.type)));
const otherFunctions = await getFunctionsAndTheirCodesAsync(otherFunctionNames, isDotNet, projectFolder, hostJsonFolder);
for (const orch of orchestrators) {
// Trying to match this orchestrator with its calling function
const regex = TraversalRegexes.getStartNewOrchestrationRegex(orch.name);
for (const func of otherFunctions) {
// If this function seems to be calling that orchestrator
if (!!regex.exec(func.code)) {
functions[orch.name].isCalledBy.push(func.name);
}
}
// Matching suborchestrators
for (const subOrch of orchestrators) {
if (orch.name === subOrch.name) {
continue;
}
// If this orchestrator seems to be calling that suborchestrator
const regex = TraversalRegexes.getCallSubOrchestratorRegex(subOrch.name);
if (!!regex.exec(orch.code)) {
// Mapping that suborchestrator to this orchestrator
functions[subOrch.name].isCalledBy.push(orch.name);
}
}
// Mapping activities to orchestrators
mapActivitiesToOrchestrator(functions, orch, activityNames);
// Checking whether orchestrator calls itself
if (!!TraversalRegexes.continueAsNewRegex.exec(orch.code)) {
functions[orch.name].isCalledByItself = true;
}
// Trying to map event producers with their consumers
const eventNames = getEventNames(orch.code);
for (const eventName of eventNames) {
const regex = TraversalRegexes.getRaiseEventRegex(eventName);
for (const func of otherFunctions) {
// If this function seems to be sending that event
if (!!regex.exec(func.code)) {
functions[orch.name].isSignalledBy.push({ name: func.name, signalName: eventName });
}
}
}
}
for (const entity of entities) {
// Trying to match this entity with its calling function
for (const func of otherFunctions) {
// If this function seems to be calling that entity
const regex = TraversalRegexes.getSignalEntityRegex(entity.name);
if (!!regex.exec(func.code)) {
functions[entity.name].isCalledBy.push(func.name);
}
}
}
if (isDotNet) {
// Trying to extract extra binding info from C# code
for (const func of otherFunctions) {
const moreBindings = DotNetBindingsParser.tryExtractBindings(func.code);
functions[func.name].bindings.push(...moreBindings);
}
}
// Also adding file paths and code positions
for (const func of otherFunctions.concat(orchestrators).concat(activities).concat(entities)) {
functions[func.name].filePath = func.filePath;
functions[func.name].pos = func.pos;
functions[func.name].lineNr = func.lineNr;
}
return functions;
}
// Tries to extract event names that this orchestrator is awaiting
function getEventNames(orchestratorCode: string): string[] {
const result = [];
const regex = TraversalRegexes.waitForExternalEventRegex;
var match: RegExpExecArray | null;
while (!!(match = regex.exec(orchestratorCode))) {
result.push(match[4]);
}
return result;
}
// Tries to load code for functions of certain type
async function getFunctionsAndTheirCodesAsync(functionNames: string[], isDotNet: boolean, projectFolder: string, hostJsonFolder: string)
: Promise<{ name: string, code: string, filePath: string, pos: number, lineNr: number }[]> {
const promises = functionNames.map(async name => {
const match = await (isDotNet ?
findFileRecursivelyAsync(projectFolder, '.+\\.(f|c)s$', true, TraversalRegexes.getDotNetFunctionNameRegex(name)) :
findFileRecursivelyAsync(path.join(hostJsonFolder, name), '(index\\.ts|index\\.js|__init__\\.py)$', true));
if (!match) {
return undefined;
}
const code = !isDotNet ? match.code : getCodeInBrackets(match.code!, match.pos! + match.length!, '{', '}', ' \n');
const pos = !match.pos ? 0 : match.pos;
const lineNr = posToLineNr(match.code, pos);
return { name, code, filePath: match.filePath, pos, lineNr };
});
return (await Promise.all(promises)).filter(f => !!f) as any;
}
// Tries to match orchestrator with its activities
function mapActivitiesToOrchestrator(functions: FunctionsMap, orch: {name: string, code: string}, activityNames: string[]): void {
for (const activityName of activityNames) {
// If this orchestrator seems to be calling this activity
const regex = TraversalRegexes.getCallActivityRegex(activityName);
if (!!regex.exec(orch.code)) {
// Then mapping this activity to this orchestrator
if (!functions[activityName].isCalledBy) {
functions[activityName].isCalledBy = [];
}
functions[activityName].isCalledBy.push(orch.name);
}
}
}

Просмотреть файл

@ -0,0 +1,305 @@
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
// Does a git clone into a temp folder and returns info about that cloned code
export async function cloneFromGitHub(url: string): Promise<{gitTempFolder: string, projectFolder: string}> {
var repoName = '', branchName = '', relativePath = '', gitTempFolder = '';
var restOfUrl: string[] = [];
const match = /(https:\/\/github.com\/.*?)\/([^\/]+)(\/tree\/)?(.*)/i.exec(url);
if (!match || match.length < 5) {
// expecting repo name to be the last segment of remote origin URL
repoName = url.substr(url.lastIndexOf('/') + 1);
} else {
const orgUrl = match[1];
repoName = match[2];
if (repoName.toLowerCase().endsWith('.git')) {
repoName = repoName.substr(0, repoName.length - 4);
}
url = `${orgUrl}/${repoName}.git`;
if (!!match[4]) {
restOfUrl = match[4].split('/').filter(s => !!s);
}
}
gitTempFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'git-clone-'));
// The provided URL might contain both branch name and relative path. The only way to separate one from another
// is to repeatedly try cloning assumed branch names, until we finally succeed.
for (var i = restOfUrl.length; i > 0; i--) {
try {
const assumedBranchName = restOfUrl.slice(0, i).join('/');
execSync(`git clone ${url} --branch ${assumedBranchName}`, { cwd: gitTempFolder });
branchName = assumedBranchName;
relativePath = path.join(...restOfUrl.slice(i, restOfUrl.length));
break;
} catch {
continue;
}
}
if (!branchName) {
// Just doing a normal git clone
execSync(`git clone ${url}`, { cwd: gitTempFolder });
}
return { gitTempFolder, projectFolder: path.join(gitTempFolder, repoName, relativePath) };
}
// Primitive way of getting a line number out of symbol position
export function posToLineNr(code: string | undefined, pos: number): number {
if (!code) {
return 0;
}
const lineBreaks = code.substr(0, pos).match(/(\r\n|\r|\n)/g);
return !lineBreaks ? 1 : lineBreaks.length + 1;
}
// Checks if the given folder looks like a .Net project
export async function isDotNetProjectAsync(projectFolder: string): Promise<boolean> {
return (await fs.promises.readdir(projectFolder)).some(fn => {
fn = fn.toLowerCase();
return fn.endsWith('.sln') ||
fn.endsWith('.fsproj') ||
(fn.endsWith('.csproj') && fn !== 'extensions.csproj');
});
}
// Complements regex's inability to keep up with nested brackets
export function getCodeInBrackets(str: string, startFrom: number, openingBracket: string, closingBracket: string, mustHaveSymbols: string = ''): string {
var bracketCount = 0, openBracketPos = 0, mustHaveSymbolFound = !mustHaveSymbols;
for (var i = startFrom; i < str.length; i++) {
switch (str[i]) {
case openingBracket:
if (bracketCount <= 0) {
openBracketPos = i + 1;
}
bracketCount++;
break;
case closingBracket:
bracketCount--;
if (bracketCount <= 0 && mustHaveSymbolFound) {
return str.substring(startFrom, i + 1);
}
break;
}
if (bracketCount > 0 && mustHaveSymbols.includes(str[i])) {
mustHaveSymbolFound = true;
}
}
return '';
}
// General-purpose regexes
export class TraversalRegexes {
static getStartNewOrchestrationRegex(orchName: string): RegExp {
return new RegExp(`(StartNew|StartNewAsync|start_new)(\\s*<[\\w\\.-\\[\\]\\<\\>,\\s]+>)?\\s*\\(\\s*(["'\`]|nameof\\s*\\(\\s*[\\w\\.-]*|[\\w\\s\\.]+\\.\\s*)${orchName}\\s*["'\\),]{1}`, 'i');
}
static getCallSubOrchestratorRegex(subOrchName: string): RegExp {
return new RegExp(`(CallSubOrchestrator|CallSubOrchestratorWithRetry|call_sub_orchestrator)(Async)?(\\s*<[\\w\\.-\\[\\]\\<\\>,\\s]+>)?\\s*\\(\\s*(["'\`]|nameof\\s*\\(\\s*[\\w\\.-]*|[\\w\\s\\.]+\\.\\s*)${subOrchName}\\s*["'\\),]{1}`, 'i');
}
static readonly continueAsNewRegex = new RegExp(`ContinueAsNew\\s*\\(`, 'i');
static getRaiseEventRegex(eventName: string): RegExp {
return new RegExp(`(RaiseEvent|raise_event)(Async)?(.|\r|\n)*${eventName}`, 'i');
}
static getSignalEntityRegex(entityName: string): RegExp {
return new RegExp(`${entityName}\\s*["'>]{1}`);
}
static readonly waitForExternalEventRegex = new RegExp(`(WaitForExternalEvent|wait_for_external_event)(<[\\s\\w,\\.-\\[\\]\\(\\)\\<\\>]+>)?\\s*\\(\\s*(nameof\\s*\\(\\s*|["'\`]|[\\w\\s\\.]+\\.\\s*)?([\\s\\w\\.-]+)\\s*["'\`\\),]{1}`, 'gi');
static getDotNetFunctionNameRegex(funcName: string): RegExp {
return new RegExp(`FunctionName(Attribute)?\\s*\\(\\s*(nameof\\s*\\(\\s*|["'\`]|[\\w\\s\\.]+\\.\\s*)${funcName}\\s*["'\`\\)]{1}`)
}
static getCallActivityRegex(activityName: string): RegExp {
return new RegExp(`(CallActivity|call_activity)[\\s\\w,\\.-<>\\[\\]\\(\\)\\?]*\\([\\s\\w\\.-]*["'\`]?${activityName}\\s*["'\`\\),]{1}`, 'i');
}
}
// In .Net not all bindings are mentioned in function.json, so we need to analyze source code to extract them
export class DotNetBindingsParser {
// Extracts additional bindings info from C#/F# source code
static tryExtractBindings(funcCode: string): {type: string, direction: string}[] {
const result: {type: string, direction: string}[] = [];
if (!funcCode) {
return result;
}
const regex = this.bindingAttributeRegex;
var match: RegExpExecArray | null;
while (!!(match = regex.exec(funcCode))) {
const isReturn = !!match[2];
const attributeName = match[3];
const attributeCodeStartIndex = match.index + match[0].length - 1;
const attributeCode = getCodeInBrackets(funcCode, attributeCodeStartIndex, '(', ')', '');
this.isOutRegex.lastIndex = attributeCodeStartIndex + attributeCode.length;
const isOut = !!this.isOutRegex.exec(funcCode);
switch (attributeName) {
case 'Blob': {
const binding: any = { type: 'blob', direction: isReturn || isOut ? 'out' : 'in' };
const paramsMatch = this.blobParamsRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['path'] = paramsMatch[1];
}
result.push(binding);
break;
}
case 'Table': {
const binding: any = { type: 'table', direction: isReturn || isOut ? 'out' : 'in' };
const paramsMatch = this.singleParamRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['tableName'] = paramsMatch[2];
}
result.push(binding);
break;
}
case 'CosmosDB': {
const binding: any = { type: 'cosmosDB', direction: isReturn || isOut ? 'out' : 'in' };
const paramsMatch = this.cosmosDbParamsRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['databaseName'] = paramsMatch[1];
binding['collectionName'] = paramsMatch[3];
}
result.push(binding);
break;
}
case 'SignalRConnectionInfo': {
const binding: any = { type: 'signalRConnectionInfo', direction: 'in' };
const paramsMatch = this.signalRConnInfoParamsRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['hubName'] = paramsMatch[1];
}
result.push(binding);
break;
}
case 'EventGrid': {
const binding: any = { type: 'eventGrid', direction: 'out' };
const paramsMatch = this.eventGridParamsRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['topicEndpointUri'] = paramsMatch[1];
binding['topicKeySetting'] = paramsMatch[3];
}
result.push(binding);
break;
}
case 'EventHub': {
const binding: any = { type: 'eventHub', direction: 'out' };
const paramsMatch = this.eventHubParamsRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['eventHubName'] = paramsMatch[1];
}
result.push(binding);
break;
}
case 'Queue': {
const binding: any = { type: 'queue', direction: 'out' };
const paramsMatch = this.singleParamRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['queueName'] = paramsMatch[2];
}
result.push(binding);
break;
}
case 'ServiceBus': {
const binding: any = { type: 'serviceBus', direction: 'out' };
const paramsMatch = this.singleParamRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['queueName'] = paramsMatch[2];
}
result.push(binding);
break;
}
case 'SignalR': {
const binding: any = { type: 'signalR', direction: 'out' };
const paramsMatch = this.signalRParamsRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['hubName'] = paramsMatch[1];
}
result.push(binding);
break;
}
case 'RabbitMQ': {
const binding: any = { type: 'rabbitMQ', direction: 'out' };
const paramsMatch = this.rabbitMqParamsRegex.exec(attributeCode);
if (!!paramsMatch) {
binding['queueName'] = paramsMatch[1];
}
result.push(binding);
break;
}
case 'SendGrid': {
result.push({ type: 'sendGrid', direction: 'out' });
break;
}
case 'TwilioSms': {
result.push({ type: 'twilioSms', direction: 'out' });
break;
}
}
}
return result;
}
static readonly bindingAttributeRegex = new RegExp(`\\[(<)?\\s*(return:)?\\s*(\\w+)(Attribute)?\\s*\\(`, 'g');
static readonly singleParamRegex = new RegExp(`("|nameof\\s*\\()?([\\w\\.-]+)`);
static readonly eventHubParamsRegex = new RegExp(`"([^"]+)"`);
static readonly signalRParamsRegex = new RegExp(`"([^"]+)"`);
static readonly rabbitMqParamsRegex = new RegExp(`"([^"]+)"`);
static readonly blobParamsRegex = new RegExp(`"([^"]+)"`);
static readonly cosmosDbParamsRegex = new RegExp(`"([^"]+)"(.|\r|\n)+?"([^"]+)"`);
static readonly signalRConnInfoParamsRegex = new RegExp(`"([^"]+)"`);
static readonly eventGridParamsRegex = new RegExp(`"([^"]+)"(.|\r|\n)+?"([^"]+)"`);
static readonly isOutRegex = new RegExp(`\\]\\s*(out |ICollector|IAsyncCollector).*?(,|\\()`, 'g');
}

Просмотреть файл

@ -0,0 +1,86 @@
import * as vscode from 'vscode';
import { MonitorTreeDataProvider } from './MonitorTreeDataProvider';
import { FunctionGraphList } from './FunctionGraphList';
import { Settings } from './Settings';
var monitorTreeDataProvider: MonitorTreeDataProvider;
var functionGraphList: FunctionGraphList;
// Name for our logging OutputChannel
const OutputChannelName = 'Durable Functions Monitor';
export function activate(context: vscode.ExtensionContext) {
// For logging
const logChannel = Settings().enableLogging ? vscode.window.createOutputChannel(OutputChannelName) : undefined;
if (!!logChannel) {
context.subscriptions.push(logChannel);
}
functionGraphList = new FunctionGraphList(context, logChannel);
monitorTreeDataProvider = new MonitorTreeDataProvider(context, functionGraphList, logChannel);
context.subscriptions.push(
vscode.debug.onDidStartDebugSession(() => monitorTreeDataProvider.handleOnDebugSessionStarted()),
vscode.commands.registerCommand('extension.durableFunctionsMonitor',
() => monitorTreeDataProvider.attachToTaskHub(null)),
vscode.commands.registerCommand('extension.durableFunctionsMonitorPurgeHistory',
() => monitorTreeDataProvider.attachToTaskHub(null, { id: 'purgeHistory' })),
vscode.commands.registerCommand('extension.durableFunctionsMonitorCleanEntityStorage',
() => monitorTreeDataProvider.attachToTaskHub(null, { id: 'cleanEntityStorage' })),
vscode.commands.registerCommand('extension.durableFunctionsMonitorGotoInstanceId',
() => monitorTreeDataProvider.gotoInstanceId(null)),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.purgeHistory',
(item) => monitorTreeDataProvider.attachToTaskHub(item, { id: 'purgeHistory' })),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.cleanEntityStorage',
(item) => monitorTreeDataProvider.attachToTaskHub(item, { id: 'cleanEntityStorage' })),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.startNewInstance',
(item) => monitorTreeDataProvider.attachToTaskHub(item, { id: 'startNewInstance' })),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.attachToTaskHub',
(item) => monitorTreeDataProvider.attachToTaskHub(item)),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.detachFromTaskHub',
(item) => monitorTreeDataProvider.detachFromTaskHub(item)),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.deleteTaskHub',
(item) => monitorTreeDataProvider.deleteTaskHub(item)),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.gotoInstanceId',
(item) => monitorTreeDataProvider.gotoInstanceId(item)),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.refresh',
() => monitorTreeDataProvider.refresh()),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.attachToAnotherTaskHub',
() => monitorTreeDataProvider.attachToAnotherTaskHub()),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.detachFromAllTaskHubs',
() => monitorTreeDataProvider.detachFromAllTaskHubs()),
vscode.window.registerTreeDataProvider('durableFunctionsMonitorTreeView', monitorTreeDataProvider),
vscode.commands.registerCommand('extension.durableFunctionsMonitorVisualizeAsGraph',
(item) => functionGraphList.visualize(item)),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.openInstancesInStorageExplorer',
(item) => monitorTreeDataProvider.openTableInStorageExplorer(item, 'Instances')),
vscode.commands.registerCommand('durableFunctionsMonitorTreeView.openHistoryInStorageExplorer',
(item) => monitorTreeDataProvider.openTableInStorageExplorer(item, 'History')),
);
}
export function deactivate() {
functionGraphList.cleanup();
return monitorTreeDataProvider.cleanup();
}

Просмотреть файл

@ -0,0 +1,23 @@
import * as path from 'path';
import { runTests } from 'vscode-test';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to test runner
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error('Failed to run tests');
process.exit(1);
}
}
main();

Просмотреть файл

@ -0,0 +1,15 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.equal(-1, [1, 2, 3].indexOf(5));
assert.equal(-1, [1, 2, 3].indexOf(0));
});
});

Просмотреть файл

@ -0,0 +1,37 @@
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
});
mocha.useColors(true);
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
e(err);
}
});
});
}

Просмотреть файл

@ -0,0 +1,25 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": [
"dom",
"es6"
],
"sourceMap": true,
"rootDir": "src",
"strict": true, /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"resolveJsonModule": true
},
"exclude": [
"node_modules",
".vscode-test",
"backend"
]
}

Просмотреть файл

@ -0,0 +1,15 @@
{
"rules": {
"no-string-throw": true,
"no-unused-expression": true,
"no-duplicate-variable": true,
"curly": true,
"class-name": true,
"semicolon": [
true,
"always"
],
"triple-equals": true
},
"defaultSeverity": "warning"
}

267
durablefunctionsmonitor.dotnetbackend/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,267 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# nuget.exe
nuget.exe
# Azure Functions localsettings file
local.settings.json
# User-specific files
*.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/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.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
# 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
# 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
# TODO: 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
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable 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
# 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
node_modules/
orleans.codegen.cs
# 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
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# 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

6
durablefunctionsmonitor.dotnetbackend/.vscode/extensions.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,6 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions",
"ms-vscode.csharp"
]
}

11
durablefunctionsmonitor.dotnetbackend/.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to .NET Functions",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

7
durablefunctionsmonitor.dotnetbackend/.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
{
"azureFunctions.deploySubpath": "bin/Release/netcoreapp3.1/publish",
"azureFunctions.projectLanguage": "C#",
"azureFunctions.projectRuntime": "~2",
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.preDeployTask": "publish"
}

69
durablefunctionsmonitor.dotnetbackend/.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,69 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "clean",
"command": "dotnet",
"args": [
"clean",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"problemMatcher": "$msCompile"
},
{
"label": "build",
"command": "dotnet",
"args": [
"build",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"dependsOn": "clean",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$msCompile"
},
{
"label": "clean release",
"command": "dotnet",
"args": [
"clean",
"--configuration",
"Release",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"args": [
"publish",
"--configuration",
"Release",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"dependsOn": "clean release",
"problemMatcher": "$msCompile"
},
{
"type": "func",
"dependsOn": "build",
"options": {
"cwd": "${workspaceFolder}/bin/Debug/netcoreapp3.1"
},
"command": "host start",
"isBackground": true,
"problemMatcher": "$func-watch"
}
]
}

Просмотреть файл

@ -0,0 +1,354 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Claims;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
namespace DurableFunctionsMonitor.DotNetBackend
{
internal static class Auth
{
// Magic constant for turning auth off
public const string ISureKnowWhatIAmDoingNonce = "i_sure_know_what_i_am_doing";
// Value of WEBSITE_AUTH_UNAUTHENTICATED_ACTION config setting, when server-directed login flow is configured
public const string UnauthenticatedActionRedirectToLoginPage = "RedirectToLoginPage";
// Default user name claim name
public const string PreferredUserNameClaim = "preferred_username";
// Roles claim name
private const string RolesClaim = "roles";
// If DFM_NONCE was passed as env variable, validates that the incoming request contains it. Throws UnauthorizedAccessException, if it doesn't.
public static bool IsNonceSetAndValid(IHeaderDictionary headers)
{
// From now on it is the only way to skip auth
if (DfmEndpoint.Settings.DisableAuthentication)
{
return true;
}
string nonce = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_NONCE);
if (!string.IsNullOrEmpty(nonce))
{
// Checking the nonce header
if (nonce == headers["x-dfm-nonce"])
{
return true;
}
throw new UnauthorizedAccessException("Invalid nonce. Call is rejected.");
}
return false;
}
// Validates that the incoming request is properly authenticated
public static async Task ValidateIdentityAsync(ClaimsPrincipal principal, IHeaderDictionary headers, IRequestCookieCollection cookies, string taskHubName)
{
// First validating Task Hub name, if it was specified
if (!string.IsNullOrEmpty(taskHubName))
{
await ThrowIfTaskHubNameIsInvalid(taskHubName);
}
// Starting with nonce (used when running as a VsCode extension)
if (IsNonceSetAndValid(headers))
{
return;
}
// Then validating anti-forgery token
ThrowIfXsrfTokenIsInvalid(headers, cookies);
// Trying with EasyAuth
var userNameClaim = principal?.FindFirst(DfmEndpoint.Settings.UserNameClaimName);
if (userNameClaim == null)
{
// Validating and parsing the token ourselves
principal = await ValidateToken(headers["Authorization"]);
userNameClaim = principal.FindFirst(DfmEndpoint.Settings.UserNameClaimName);
}
if (userNameClaim == null)
{
throw new UnauthorizedAccessException($"'{DfmEndpoint.Settings.UserNameClaimName}' claim is missing in the incoming identity. Call is rejected.");
}
if (DfmEndpoint.Settings.AllowedUserNames != null)
{
if (!DfmEndpoint.Settings.AllowedUserNames.Contains(userNameClaim.Value))
{
throw new UnauthorizedAccessException($"User {userNameClaim.Value} is not mentioned in {EnvVariableNames.DFM_ALLOWED_USER_NAMES} config setting. Call is rejected");
}
}
// Also validating App Roles, if set
if (DfmEndpoint.Settings.AllowedAppRoles != null)
{
var roleClaims = principal.FindAll(RolesClaim);
if (!roleClaims.Any(claim => DfmEndpoint.Settings.AllowedAppRoles.Contains(claim.Value)))
{
throw new UnauthorizedAccessException($"User {userNameClaim.Value} doesn't have any of roles mentioned in {EnvVariableNames.DFM_ALLOWED_APP_ROLES} config setting. Call is rejected");
}
}
}
private static async Task<HashSet<string>> GetTaskHubNamesFromStorage(string connStringName)
{
var tableNames = await TableClient.GetTableClient(connStringName).ListTableNamesAsync();
var hubNames = new HashSet<string>(tableNames
.Where(n => n.EndsWith("Instances"))
.Select(n => n.Remove(n.Length - "Instances".Length)),
StringComparer.InvariantCultureIgnoreCase);
hubNames.IntersectWith(tableNames
.Where(n => n.EndsWith("History"))
.Select(n => n.Remove(n.Length - "History".Length)));
return hubNames;
}
// Lists all allowed Task Hubs. The returned HashSet is configured to ignore case.
public static async Task<HashSet<string>> GetAllowedTaskHubNamesAsync()
{
// Respecting DFM_HUB_NAME, if it is set
string dfmHubName = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_HUB_NAME);
if (!string.IsNullOrEmpty(dfmHubName))
{
return new HashSet<string>(dfmHubName.Split(','), StringComparer.InvariantCultureIgnoreCase);
}
// Also respecting host.json setting, when set
dfmHubName = TryGetHubNameFromHostJson();
if (!string.IsNullOrEmpty(dfmHubName))
{
return new HashSet<string>(new string[] { dfmHubName }, StringComparer.InvariantCultureIgnoreCase);
}
// Otherwise trying to load table names from the Storage
try
{
var hubNames = await GetTaskHubNamesFromStorage(EnvVariableNames.AzureWebJobsStorage);
// Also checking alternative connection strings
foreach(var connName in AlternativeConnectionStringNames)
{
var connAndHubNames = (await GetTaskHubNamesFromStorage(Globals.GetFullConnectionStringEnvVariableName(connName)))
.Select(hubName => Globals.CombineConnNameAndHubName(connName, hubName));
hubNames.UnionWith(connAndHubNames);
}
return hubNames;
}
catch (Exception)
{
// Intentionally returning null. Need to skip validation, if for some reason list of tables
// cannot be loaded from Storage. But only in that case.
return null;
}
}
private static readonly Regex ValidTaskHubNameRegex = new Regex(@"^[\w-]{3,128}$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static Task<HashSet<string>> HubNamesTask = GetAllowedTaskHubNamesAsync();
// Checks that a Task Hub name looks like a Task Hub name
public static void ThrowIfTaskHubNameHasInvalidSymbols(string hubName)
{
if (!ValidTaskHubNameRegex.Match(hubName).Success)
{
throw new ArgumentException($"Task Hub name is invalid.");
}
}
// Checks that a Task Hub name is valid for this instace
public static async Task ThrowIfTaskHubNameIsInvalid(string hubName)
{
// Two bugs away. Validating that the incoming Task Hub name looks like a Task Hub name
ThrowIfTaskHubNameHasInvalidSymbols(hubName);
var hubNames = await HubNamesTask;
if (hubNames == null || !hubNames.Contains(hubName))
{
// doing double-check, by reloading hub names
HubNamesTask = GetAllowedTaskHubNamesAsync();
hubNames = await HubNamesTask;
}
// If existing Task Hub names cannot be read from Storage, we can only skip validation and return true.
// Note, that it will never be null, if DFM_HUB_NAME is set. So authZ is always in place.
if (hubNames == null)
{
return;
}
if (!hubNames.Contains(hubName))
{
throw new UnauthorizedAccessException($"Task Hub '{hubName}' is not allowed.");
}
}
// Compares our XSRF tokens, that come from cookies and headers
public static void ThrowIfXsrfTokenIsInvalid(IHeaderDictionary headers, IRequestCookieCollection cookies)
{
string tokenFromHeaders = headers[Globals.XsrfTokenCookieAndHeaderName];
if (string.IsNullOrEmpty(tokenFromHeaders))
{
throw new UnauthorizedAccessException("XSRF token is missing.");
}
string tokenFromCookies = cookies[Globals.XsrfTokenCookieAndHeaderName];
if (tokenFromCookies != tokenFromHeaders)
{
throw new UnauthorizedAccessException("XSRF tokens do not match.");
}
}
internal static string[] AlternativeConnectionStringNames = GetAlternativeConnectionStringNames().ToArray();
internal static IEnumerable<string> GetAlternativeConnectionStringNames()
{
var envVars = Environment.GetEnvironmentVariables();
foreach(DictionaryEntry kv in envVars)
{
string variableName = kv.Key.ToString();
if (variableName.StartsWith(EnvVariableNames.DFM_ALTERNATIVE_CONNECTION_STRING_PREFIX))
{
yield return variableName.Substring(EnvVariableNames.DFM_ALTERNATIVE_CONNECTION_STRING_PREFIX.Length);
}
}
}
private static string TryGetHubNameFromHostJson()
{
try
{
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
string functionAppFolder = Path.GetDirectoryName(Path.GetDirectoryName(assemblyLocation));
string hostJsonFileName = Path.Combine(functionAppFolder, "host.json");
dynamic hostJson = JObject.Parse(File.ReadAllText(hostJsonFileName));
string hubName = hostJson.extensions.durableTask.hubName;
if (hubName.StartsWith('%') && hubName.EndsWith('%'))
{
hubName = Environment.GetEnvironmentVariable(hubName.Trim('%'));
}
return hubName;
}
catch (Exception)
{
return string.Empty;
}
}
internal static JwtSecurityTokenHandler MockedJwtSecurityTokenHandler = null;
private static async Task<ClaimsPrincipal> ValidateToken(string authorizationHeader)
{
if (string.IsNullOrEmpty(authorizationHeader))
{
throw new UnauthorizedAccessException("No access token provided. Call is rejected.");
}
string clientId = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_CLIENT_ID);
if (string.IsNullOrEmpty(clientId))
{
throw new UnauthorizedAccessException($"Specify the Valid Audience value via '{EnvVariableNames.WEBSITE_AUTH_CLIENT_ID}' config setting. Typically it is the ClientId of your AAD application.");
}
string openIdIssuer = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER);
if (string.IsNullOrEmpty(openIdIssuer))
{
throw new UnauthorizedAccessException($"Specify the Valid Issuer value via '{EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER}' config setting. Typically it looks like 'https://login.microsoftonline.com/<your-aad-tenant-id>/v2.0'.");
}
string token = authorizationHeader.Substring("Bearer ".Length);
var validationParameters = new TokenValidationParameters
{
ValidAudiences = new[] { clientId },
ValidIssuers = new[] { openIdIssuer },
// Yes, it is OK to await a Task multiple times like this
IssuerSigningKeys = await GetSigningKeysTask,
// According to internet, this should not be needed (despite the fact that the default value is false)
// But better to be two-bugs away
ValidateIssuerSigningKey = true
};
var handler = MockedJwtSecurityTokenHandler ?? new JwtSecurityTokenHandler();
return handler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
}
// Caching the keys for 24 hours
internal static Task<ICollection<SecurityKey>> GetSigningKeysTask = InitGetSigningKeysTask(86400, 0);
internal static Task<ICollection<SecurityKey>> InitGetSigningKeysTask(int cacheTtlInSeconds, int retryCount = 0)
{
// If you ever use this code in Asp.Net, don't forget to wrap this line with Task.Run(), to decouple from SynchronizationContext
var task = GetSigningKeysAsync();
// Adding cache-flushing continuation
task.ContinueWith(async t =>
{
// If the data retrieval failed, then retrying immediately, but no more than 3 times.
// Otherwise re-populating the cache in cacheTtlInSeconds.
if (t.IsFaulted)
{
if (retryCount > 1)
{
return;
}
}
else
{
await Task.Delay(TimeSpan.FromSeconds(cacheTtlInSeconds));
}
GetSigningKeysTask = InitGetSigningKeysTask(cacheTtlInSeconds, t.IsFaulted ? retryCount + 1 : 0);
});
return task;
}
private static async Task<ICollection<SecurityKey>> GetSigningKeysAsync()
{
string openIdIssuer = Environment.GetEnvironmentVariable(EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER);
if (string.IsNullOrEmpty(openIdIssuer))
{
throw new UnauthorizedAccessException($"Specify the Valid Issuer value via '{EnvVariableNames.WEBSITE_AUTH_OPENID_ISSUER}' config setting. Typically it looks like 'https://login.microsoftonline.com/<your-aad-tenant-id>/v2.0'.");
}
if (openIdIssuer.EndsWith("/v2.0"))
{
openIdIssuer = openIdIssuer.Substring(0, openIdIssuer.Length - "/v2.0".Length);
}
string stsDiscoveryEndpoint = $"{openIdIssuer}/.well-known/openid-configuration";
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
var config = await configManager.GetConfigurationAsync();
return config.SigningKeys;
}
}
}

Просмотреть файл

@ -0,0 +1,321 @@
using System;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
using Microsoft.WindowsAzure.Storage;
using System.IO;
using System.Text;
using System.Collections.Concurrent;
using System.Reflection;
namespace DurableFunctionsMonitor.DotNetBackend
{
// Contains all logic of loading custom tab/html templates
// TODO: respect alternative connection strings
class CustomTemplates
{
internal static Task<LiquidTemplatesMap> GetTabTemplatesAsync()
{
if (TabTemplatesTask == null)
{
TabTemplatesTask = string.IsNullOrEmpty(DfmEndpoint.Settings.CustomTemplatesFolderName) ?
GetTabTemplatesFromStorageAsync() : GetTabTemplatesFromFolderAsync(DfmEndpoint.Settings.CustomTemplatesFolderName);
}
return TabTemplatesTask;
}
internal static Task<string> GetCustomMetaTagCodeAsync()
{
if (CustomMetaTagCodeTask == null)
{
CustomMetaTagCodeTask = string.IsNullOrEmpty(DfmEndpoint.Settings.CustomTemplatesFolderName) ?
GetCustomMetaTagCodeFromStorageAsync() : GetCustomMetaTagCodeFromFolderAsync(DfmEndpoint.Settings.CustomTemplatesFolderName);
}
return CustomMetaTagCodeTask;
}
internal static Task<FunctionMapsMap> GetFunctionMapsAsync()
{
if (FunctionMapsTask == null)
{
FunctionMapsTask = string.IsNullOrEmpty(DfmEndpoint.Settings.CustomTemplatesFolderName) ?
GetFunctionMapsFromStorageAsync() : GetFunctionMapsFromFolderAsync(DfmEndpoint.Settings.CustomTemplatesFolderName);
}
return FunctionMapsTask;
}
// Yes, it is OK to use Task in this way.
// The Task code will only be executed once. All subsequent/parallel awaits will get the same returned value.
// Tasks do have the same behavior as Lazy<T>.
private static Task<LiquidTemplatesMap> TabTemplatesTask;
private static Task<string> CustomMetaTagCodeTask;
private static Task<FunctionMapsMap> FunctionMapsTask;
// Tries to load liquid templates from underlying Azure Storage
private static async Task<LiquidTemplatesMap> GetTabTemplatesFromStorageAsync()
{
var result = new LiquidTemplatesMap();
try
{
string connectionString = Environment.GetEnvironmentVariable(EnvVariableNames.AzureWebJobsStorage);
var blobClient = CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient();
// Listing all blobs in durable-functions-monitor/tab-templates folder
var container = blobClient.GetContainerReference(Globals.TemplateContainerName);
string templateFolderName = Globals.TabTemplateFolderName + "/";
var templateNames = await container.ListBlobsAsync(templateFolderName);
// Loading blobs in parallel
await Task.WhenAll(templateNames.Select(async templateName =>
{
var blob = await blobClient.GetBlobReferenceFromServerAsync(templateName.Uri);
// Expecting the blob name to be like "[Tab Name].[EntityTypeName].liquid" or just "[Tab Name].liquid"
var nameParts = blob.Name.Substring(templateFolderName.Length).Split('.');
if (nameParts.Length < 2 || nameParts.Last() != "liquid")
{
return;
}
string tabName = nameParts[0];
string entityTypeName = nameParts.Length > 2 ? nameParts[1] : string.Empty;
using (var stream = new MemoryStream())
{
await blob.DownloadToStreamAsync(stream);
string templateText = Encoding.UTF8.GetString(stream.ToArray());
result.GetOrAdd(entityTypeName, new ConcurrentDictionary<string, string>())[tabName] = templateText;
}
}));
}
catch (Exception)
{
// Intentionally swallowing all exceptions here
}
return result;
}
// Tries to load liquid templates from local folder
private static async Task<LiquidTemplatesMap> GetTabTemplatesFromFolderAsync(string folderName)
{
var result = new LiquidTemplatesMap();
string binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string templatesFolder = Path.Combine(binFolder, "..", folderName, Globals.TabTemplateFolderName);
if (!Directory.Exists(templatesFolder))
{
return result;
}
foreach(var templateFilePath in Directory.EnumerateFiles(templatesFolder, "*.liquid"))
{
var nameParts = Path.GetFileName(templateFilePath).Split('.');
if(nameParts.Length < 2)
{
continue;
}
string tabName = nameParts[0];
string entityTypeName = nameParts.Length > 2 ? nameParts[1] : string.Empty;
string templateText = await File.ReadAllTextAsync(templateFilePath);
result.GetOrAdd(entityTypeName, new ConcurrentDictionary<string, string>())[tabName] = templateText;
}
return result;
}
// Tries to load code for our meta tag from Storage
private static async Task<string> GetCustomMetaTagCodeFromStorageAsync()
{
try
{
var blobClient = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable(EnvVariableNames.AzureWebJobsStorage)).CreateCloudBlobClient();
var container = blobClient.GetContainerReference(Globals.TemplateContainerName);
var blob = container.GetBlobReference(Globals.CustomMetaTagBlobName);
if (!(await blob.ExistsAsync()))
{
return null;
}
using (var stream = new MemoryStream())
{
await blob.DownloadToStreamAsync(stream);
return Encoding.UTF8.GetString(stream.ToArray());
}
}
catch (Exception)
{
// Intentionally swallowing all exceptions here
return null;
}
}
// Tries to load code for our meta tag from local folder
private static async Task<string> GetCustomMetaTagCodeFromFolderAsync(string folderName)
{
string binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string filePath = Path.Combine(binFolder, "..", folderName, Globals.CustomMetaTagBlobName);
if(!File.Exists(filePath))
{
return null;
}
return await File.ReadAllTextAsync(filePath);
}
// Tries to load Function Maps from underlying Azure Storage
private static async Task<FunctionMapsMap> GetFunctionMapsFromStorageAsync()
{
var result = new FunctionMapsMap();
try
{
string connectionString = Environment.GetEnvironmentVariable(EnvVariableNames.AzureWebJobsStorage);
var blobClient = CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient();
// Listing all blobs in durable-functions-monitor/function-maps folder
var container = blobClient.GetContainerReference(Globals.TemplateContainerName);
string functionMapFolderName = Globals.FunctionMapFolderName + "/";
var fileNames = await container.ListBlobsAsync(functionMapFolderName);
// Loading blobs in parallel
await Task.WhenAll(fileNames.Select(async templateName =>
{
var blob = await blobClient.GetBlobReferenceFromServerAsync(templateName.Uri);
// Expecting the blob name to be like "dfm-function-map.[TaskHubName].json" or just "dfm-function-map.json"
var nameParts = blob.Name.Substring(functionMapFolderName.Length).Split('.');
if (nameParts.Length < 2 || nameParts.First() != Globals.FunctionMapFilePrefix || nameParts.Last() != "json")
{
return;
}
string taskHubName = nameParts.Length > 2 ? nameParts[1] : string.Empty;
using (var stream = new MemoryStream())
{
await blob.DownloadToStreamAsync(stream);
string templateText = Encoding.UTF8.GetString(stream.ToArray());
result.TryAdd(taskHubName, templateText);
}
}));
}
catch (Exception)
{
// Intentionally swallowing all exceptions here
}
return result;
}
// Tries to load Function Maps from local folder
private static async Task<FunctionMapsMap> GetFunctionMapsFromFolderAsync(string folderName)
{
var result = new FunctionMapsMap();
string binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string functionMapsFolder = Path.Combine(binFolder, "..", folderName, Globals.FunctionMapFolderName);
if (!Directory.Exists(functionMapsFolder))
{
return result;
}
foreach(var filePath in Directory.EnumerateFiles(functionMapsFolder, $"{Globals.FunctionMapFilePrefix}*.json"))
{
var nameParts = Path.GetFileName(filePath).Split('.');
if (nameParts.Length < 2)
{
continue;
}
string taskHubName = nameParts.Length > 2 ? nameParts[1] : string.Empty;
string json = await File.ReadAllTextAsync(filePath);
result.TryAdd(taskHubName, json);
}
return result;
}
}
// Represents the liquid template map
class LiquidTemplatesMap: ConcurrentDictionary<string, IDictionary<string, string>>
{
public List<string> GetTemplateNames(string entityTypeName)
{
var result = new List<string>();
IDictionary<string, string> templates;
// Getting template names for all entity types
if (this.TryGetValue(string.Empty, out templates))
{
result.AddRange(templates.Keys);
}
// Getting template names for this particular entity type
if (this.TryGetValue(entityTypeName, out templates))
{
result.AddRange(templates.Keys);
}
result.Sort();
return result;
}
public string GetTemplate(string entityTypeName, string templateName)
{
string result = null;
IDictionary<string, string> templates;
// Getting template names for all entity types
if (this.TryGetValue(string.Empty, out templates))
{
if(templates.TryGetValue(templateName, out result)){
return result;
}
}
// Getting template names for this particular entity type
if (this.TryGetValue(entityTypeName, out templates))
{
if (templates.TryGetValue(templateName, out result))
{
return result;
}
}
return result;
}
}
// Represents the map of Function Maps
class FunctionMapsMap : ConcurrentDictionary<string, string>
{
public string GetFunctionMap(string taskHubName)
{
string result = null;
// Getting Function Map for this particular Task Hub
if (!this.TryGetValue(taskHubName, out result))
{
// Getting Function Map for all Task Hubs
this.TryGetValue(string.Empty, out result);
}
return result;
}
}
}

Просмотреть файл

@ -0,0 +1,99 @@
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using System;
using Microsoft.WindowsAzure.Storage;
using System.IO;
using Newtonsoft.Json;
using System.IO.Compression;
namespace DurableFunctionsMonitor.DotNetBackend
{
// Adds extra fields to DurableOrchestrationStatus returned by IDurableClient.GetStatusAsync()
class DetailedOrchestrationStatus : DurableOrchestrationStatus
{
public EntityTypeEnum EntityType { get; private set; }
public EntityId? EntityId { get; private set; }
public List<string> TabTemplateNames
{
get
{
// The underlying Task never throws, so it's OK.
var templatesMap = CustomTemplates.GetTabTemplatesAsync().Result;
return templatesMap.GetTemplateNames(this.GetEntityTypeName());
}
}
public DetailedOrchestrationStatus(DurableOrchestrationStatus that, string connName)
{
this.Name = that.Name;
this.InstanceId = that.InstanceId;
this.CreatedTime = that.CreatedTime;
this.LastUpdatedTime = that.LastUpdatedTime;
this.RuntimeStatus = that.RuntimeStatus;
this.Output = that.Output;
this.CustomStatus = that.CustomStatus;
this.History = that.History;
// Detecting whether it is an Orchestration or a Durable Entity
var match = ExpandedOrchestrationStatus.EntityIdRegex.Match(this.InstanceId);
if (match.Success)
{
this.EntityType = EntityTypeEnum.DurableEntity;
this.EntityId = new EntityId(match.Groups[1].Value, match.Groups[2].Value);
}
this.Input = this.ConvertInput(that.Input, connName);
}
internal string GetEntityTypeName()
{
return this.EntityType == EntityTypeEnum.DurableEntity ? this.EntityId.Value.EntityName : this.Name;
}
private JToken ConvertInput(JToken input, string connName)
{
if (this.EntityType != EntityTypeEnum.DurableEntity)
{
return input;
}
// Temp fix for https://github.com/Azure/azure-functions-durable-extension/issues/1786
if (input.Type == JTokenType.String && input.ToString().ToLowerInvariant().StartsWith("https://"))
{
string connectionString = Environment.GetEnvironmentVariable(Globals.GetFullConnectionStringEnvVariableName(connName));
var blobClient = CloudStorageAccount.Parse(connectionString).CreateCloudBlobClient();
var blob = blobClient.GetBlobReferenceFromServerAsync(new Uri(input.ToString())).Result;
using (var stream = new MemoryStream())
{
blob.DownloadToStreamAsync(stream).Wait();
stream.Position = 0;
using (var gzipStream = new GZipStream(stream, CompressionMode.Decompress))
using (var streamReader = new StreamReader(gzipStream))
using (var jsonTextReader = new JsonTextReader(streamReader))
{
input = JToken.ReadFrom(jsonTextReader);
}
}
}
var stateToken = input["state"];
if (stateToken == null || stateToken.Type != JTokenType.String)
{
return input;
}
var stateString = stateToken.Value<string>();
if (!(stateString.StartsWith('{') && stateString.EndsWith('}')))
{
return input;
}
// Converting JSON string into JSON object
input["state"] = JObject.Parse(stateString);
return input;
}
}
}

Просмотреть файл

@ -0,0 +1,103 @@
using System;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace DurableFunctionsMonitor.DotNetBackend
{
enum EntityTypeEnum
{
Orchestration = 0,
DurableEntity
}
// Adds extra fields to DurableOrchestrationStatus returned by IDurableClient.ListInstancesAsync()
class ExpandedOrchestrationStatus : DurableOrchestrationStatus
{
public static readonly Regex EntityIdRegex = new Regex(@"@(\w+)@(.+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public EntityTypeEnum EntityType { get; private set; }
public EntityId? EntityId { get; private set; }
public double Duration { get; private set; }
public string LastEvent
{
get
{
if (this._detailsTask == null)
{
return string.Empty;
}
if (this._lastEvent != null)
{
return this._lastEvent;
}
this._lastEvent = string.Empty;
DurableOrchestrationStatus details;
try
{
// For some orchestrations getting an extended status might fail due to bugs in DurableOrchestrationClient.
// So just returning an empty string in that case.
details = this._detailsTask.Result;
}
catch(Exception)
{
return this._lastEvent;
}
if (details.History == null)
{
return this._lastEvent;
}
var lastEvent = details.History
.Select(e => e["Name"] ?? e["FunctionName"] )
.LastOrDefault(e => e != null);
if (lastEvent == null)
{
return this._lastEvent;
}
this._lastEvent = lastEvent.ToString();
return this._lastEvent;
}
}
public ExpandedOrchestrationStatus(DurableOrchestrationStatus that,
Task<DurableOrchestrationStatus> detailsTask,
HashSet<string> hiddenColumns)
{
this.Name = that.Name;
this.InstanceId = that.InstanceId;
this.CreatedTime = that.CreatedTime;
this.LastUpdatedTime = that.LastUpdatedTime;
this.Duration = Math.Round((that.LastUpdatedTime - that.CreatedTime).TotalMilliseconds);
this.RuntimeStatus = that.RuntimeStatus;
this.Input = hiddenColumns.Contains("input") ? null : that.Input;
this.Output = hiddenColumns.Contains("output") ? null : that.Output;
this.CustomStatus = hiddenColumns.Contains("customStatus") ? null : that.CustomStatus;
// Detecting whether it is an Orchestration or a Durable Entity
var match = EntityIdRegex.Match(this.InstanceId);
if(match.Success)
{
this.EntityType = EntityTypeEnum.DurableEntity;
this.EntityId = new EntityId(match.Groups[1].Value, match.Groups[2].Value);
}
this._detailsTask = detailsTask;
}
internal string GetEntityTypeName()
{
return this.EntityType == EntityTypeEnum.DurableEntity ? this.EntityId.Value.EntityName : this.Name;
}
private Task<DurableOrchestrationStatus> _detailsTask;
private string _lastEvent;
}
}

Просмотреть файл

@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
namespace DurableFunctionsMonitor.DotNetBackend
{
// A parsed $filter clause
class FilterClause
{
public FilterClause(string filterString)
{
if (filterString == null)
{
filterString = string.Empty;
}
filterString = this.ExtractTimeRange(filterString);
filterString = this.ExtractRuntimeStatuses(filterString);
this.ExtractPredicate(filterString);
}
public Func<string, bool> Predicate { get; private set; }
public string FieldName { get; private set; }
public DateTime? TimeFrom { get; private set; }
public DateTime? TimeTill { get; private set; }
public string[] RuntimeStatuses { get; private set; }
private string ExtractTimeRange(string filterClause)
{
this.TimeFrom = null;
this.TimeTill = null;
if (string.IsNullOrEmpty(filterClause))
{
return filterClause;
}
var match = TimeFromRegex.Match(filterClause);
if (match.Success)
{
this.TimeFrom = DateTime.Parse(match.Groups[3].Value);
filterClause = filterClause.Substring(0, match.Index) + filterClause.Substring(match.Index + match.Length);
}
match = TimeTillRegex.Match(filterClause);
if (match.Success)
{
this.TimeTill = DateTime.Parse(match.Groups[2].Value);
filterClause = filterClause.Substring(0, match.Index) + filterClause.Substring(match.Index + match.Length);
}
return filterClause;
}
private string ExtractRuntimeStatuses(string filterClause)
{
this.RuntimeStatuses = null;
if (string.IsNullOrEmpty(filterClause))
{
return filterClause;
}
var match = RuntimeStatusRegex.Match(filterClause);
if (match.Success)
{
this.RuntimeStatuses = match.Groups[2].Value
.Split(',').Where(s => !string.IsNullOrEmpty(s))
.Select(s => s.Trim(' ', '\'')).ToArray();
filterClause = filterClause.Substring(0, match.Index) + filterClause.Substring(match.Index + match.Length);
}
return filterClause;
}
private void ExtractPredicate(string filterString)
{
// startswith(field-name, 'value') eq true|false
var match = StartsWithRegex.Match(filterString);
if (match.Success)
{
bool result = true;
if (match.Groups.Count > 4)
{
result = match.Groups[4].Value != "false";
}
string arg = match.Groups[2].Value;
this.Predicate = (v) => v.StartsWith(arg) == result;
}
// contains(field-name, 'value') eq true|false
else if ((match = ContainsRegex.Match(filterString)).Success)
{
bool result = true;
if (match.Groups.Count > 4)
{
result = match.Groups[4].Value != "false";
}
string arg = match.Groups[2].Value;
this.Predicate = (v) => v.Contains(arg) == result;
}
// field-name eq|ne 'value'
else if ((match = EqRegex.Match(filterString)).Success)
{
string value = match.Groups[3].Value;
string op = match.Groups[2].Value;
this.Predicate = (v) =>
{
bool res = value == "null" ? string.IsNullOrEmpty(v) : v == value;
return op == "ne" ? !res : res;
};
}
// field-name in ('value1','value2','value3')
else if ((match = InRegex.Match(filterString)).Success)
{
string value = match.Groups[2].Value.Trim();
string[] values;
if (value.StartsWith("'"))
{
values = LazyQuotesRegex.Matches(value)
.Select(m => m.Groups[1].Value)
.ToArray();
}
else
{
values = match.Groups[2].Value
.Split(',').Where(s => !string.IsNullOrEmpty(s))
.Select(s => s.Trim(' ', '\''))
.ToArray();
}
if ((match.Groups.Count) > 4 && (match.Groups[4].Value == "false"))
{
this.Predicate = (v) => !values.Contains(v);
}
else
{
this.Predicate = (v) => values.Contains(v);
}
}
if (this.Predicate != null)
{
this.FieldName = match.Groups[1].Value;
}
}
private static readonly Regex StartsWithRegex = new Regex(@"startswith\s*\(\s*(\w+)\s*,\s*'([^']+)'\s*\)\s*(eq)?\s*(true|false)?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ContainsRegex = new Regex(@"contains\s*\(\s*(\w+)\s*,\s*'([^']+)'\s*\)\s*(eq)?\s*(true|false)?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex EqRegex = new Regex(@"(\w+)\s+(eq|ne)\s*'([^']+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex InRegex = new Regex(@"(\w+)\s+in\s*\((.*)\)\s*(eq)?\s*(true|false)?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex LazyQuotesRegex = new Regex(@"'(.*?)'", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex RuntimeStatusRegex = new Regex(@"\s*(and\s+)?runtimeStatus\s+in\s*\(([^\)]*)\)(\s*and)?\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex TimeFromRegex = new Regex(@"\s*(and\s+)?(createdTime|timestamp)\s+ge\s+'([\d-:.T]{19,}Z)'(\s*and)?\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex TimeTillRegex = new Regex(@"\s*(and\s+)?createdTime\s+le\s+'([\d-:.T]{19,}Z)'(\s*and)?\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
static class FilterClauseExtensions
{
// Applies a filter to a collection of items
internal static IEnumerable<T> ApplyFilter<T>(this IEnumerable<T> items, FilterClause filter)
{
if (string.IsNullOrEmpty(filter.FieldName))
{
// if field to be filtered is not specified, returning everything
foreach (var orchestration in items)
{
yield return orchestration;
}
}
else
{
if (filter.Predicate == null)
{
// if filter expression is invalid, returning nothing
yield break;
}
var propInfo = typeof(T).GetProperties()
.FirstOrDefault(p => p.Name.Equals(filter.FieldName, StringComparison.InvariantCultureIgnoreCase));
if (propInfo == null)
{
// if field name is invalid, returning nothing
yield break;
}
foreach (var item in items)
{
if (filter.Predicate(item.GetPropertyValueAsString(propInfo)))
{
yield return item;
}
}
}
}
// Helper for formatting orchestration field values
internal static string GetPropertyValueAsString<T>(this T orchestration, PropertyInfo propInfo)
{
object propValue = propInfo.GetValue(orchestration);
if (propValue == null)
{
return string.Empty;
}
// Explicitly handling DateTime as 'yyyy-MM-ddTHH:mm:ssZ'
if (propInfo.PropertyType == typeof(DateTime))
{
return ((DateTime)propValue).ToString(Globals.SerializerSettings.DateFormatString);
}
return propValue.ToString();
}
}
}

Просмотреть файл

@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.WindowsAzure.Storage.Table;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System.Runtime.CompilerServices;
using System.Linq;
[assembly: InternalsVisibleToAttribute("durablefunctionsmonitor.dotnetbackend.tests")]
namespace DurableFunctionsMonitor.DotNetBackend
{
static class EnvVariableNames
{
public const string AzureWebJobsStorage = "AzureWebJobsStorage";
public const string WEBSITE_SITE_NAME = "WEBSITE_SITE_NAME";
public const string WEBSITE_AUTH_CLIENT_ID = "WEBSITE_AUTH_CLIENT_ID";
public const string WEBSITE_AUTH_OPENID_ISSUER = "WEBSITE_AUTH_OPENID_ISSUER";
public const string WEBSITE_AUTH_UNAUTHENTICATED_ACTION = "WEBSITE_AUTH_UNAUTHENTICATED_ACTION";
public const string DFM_ALLOWED_USER_NAMES = "DFM_ALLOWED_USER_NAMES";
public const string DFM_ALLOWED_APP_ROLES = "DFM_ALLOWED_APP_ROLES";
public const string DFM_HUB_NAME = "DFM_HUB_NAME";
public const string DFM_NONCE = "DFM_NONCE";
public const string DFM_CLIENT_CONFIG = "DFM_CLIENT_CONFIG";
public const string DFM_MODE = "DFM_MODE";
public const string DFM_USERNAME_CLAIM_NAME = "DFM_USERNAME_CLAIM_NAME";
public const string DFM_ALTERNATIVE_CONNECTION_STRING_PREFIX = "DFM_ALTERNATIVE_CONNECTION_STRING_";
}
static class Globals
{
public const string XsrfTokenCookieAndHeaderName = "x-dfm-xsrf-token";
public const string TemplateContainerName = "durable-functions-monitor";
public const string TabTemplateFolderName = "tab-templates";
public const string FunctionMapFolderName = "function-maps";
public const string FunctionMapFilePrefix = "dfm-func-map";
public const string CustomMetaTagBlobName = "custom-meta-tag.htm";
public const string ConnAndTaskHubNameSeparator = "-";
public const string HubNameRouteParamName = "{hubName}";
// Constant, that defines the /a/p/i/{connName}-{hubName} route prefix, to let Functions Host distinguish api methods from statics
public const string ApiRoutePrefix = "a/p/i/{connName}-{hubName}";
public static void SplitConnNameAndHubName(string connAndHubName, out string connName, out string hubName)
{
int pos = connAndHubName.LastIndexOf("-");
if (pos < 0)
{
connName = null;
hubName = connAndHubName;
}
else
{
connName = connAndHubName.Substring(0, pos);
hubName = connAndHubName.Substring(pos + 1);
}
}
public static string CombineConnNameAndHubName(string connName, string hubName)
{
if (string.IsNullOrEmpty(connName) || connName == "-")
{
return hubName;
}
return $"{connName}{ConnAndTaskHubNameSeparator}{hubName}";
}
public static bool IsDefaultConnectionStringName(string connName)
{
return string.IsNullOrEmpty(connName) || connName == "-";
}
public static string GetFullConnectionStringEnvVariableName(string connName)
{
if (IsDefaultConnectionStringName(connName))
{
return EnvVariableNames.AzureWebJobsStorage;
}
else
{
return EnvVariableNames.DFM_ALTERNATIVE_CONNECTION_STRING_PREFIX + connName;
}
}
// Applies authN/authZ rules and handles incoming HTTP request. Also does error handling.
public static async Task<IActionResult> HandleAuthAndErrors(this HttpRequest req, string connName, string hubName, ILogger log, Func<Task<IActionResult>> todo)
{
return await HandleErrors(req, log, async () => {
await Auth.ValidateIdentityAsync(req.HttpContext.User, req.Headers, req.Cookies, CombineConnNameAndHubName(connName, hubName));
return await todo();
});
}
// Handles incoming HTTP request with error handling.
public static async Task<IActionResult> HandleErrors(this HttpRequest req, ILogger log, Func<Task<IActionResult>> todo)
{
try
{
return await todo();
}
catch (UnauthorizedAccessException ex)
{
log.LogError(ex, $"DFM failed to authenticate request");
return new UnauthorizedResult();
}
catch (Exception ex)
{
log.LogError(ex, "DFM failed");
return new BadRequestObjectResult(ex.Message);
}
}
// Lists all blobs from Azure Blob Container
public static async Task<IEnumerable<IListBlobItem>> ListBlobsAsync(this CloudBlobContainer container, string prefix)
{
var result = new List<IListBlobItem>();
BlobContinuationToken token = null;
do
{
var nextBatch = await container.ListBlobsSegmentedAsync(prefix, token);
result.AddRange(nextBatch.Results);
token = nextBatch.ContinuationToken;
}
while (token != null);
return result;
}
// Fighting with https://github.com/Azure/azure-functions-durable-js/issues/94
// Could use a custom JsonConverter, but it won't be invoked for nested items :(
public static string FixUndefinedsInJson(this string json)
{
return json.Replace("\": undefined", "\": null");
}
// Shared JSON serialization settings
public static JsonSerializerSettings SerializerSettings = GetSerializerSettings();
// A customized way of returning JsonResult, to cope with Functions v2/v3 incompatibility
public static ContentResult ToJsonContentResult(this object result, Func<string, string> applyThisToJson = null)
{
string json = JsonConvert.SerializeObject(result, Globals.SerializerSettings);
if(applyThisToJson != null)
{
json = applyThisToJson(json);
}
return new ContentResult() { Content = json, ContentType = "application/json" };
}
public static IEnumerable<T> ApplyTop<T>(this IEnumerable<T> collection, IQueryCollection query)
{
var clause = query["$top"];
return clause.Any() ? collection.Take(int.Parse(clause)) : collection;
}
public static IEnumerable<T> ApplySkip<T>(this IEnumerable<T> collection, IQueryCollection query)
{
var clause = query["$skip"];
return clause.Any() ? collection.Skip(int.Parse(clause)) : collection;
}
private static JsonSerializerSettings GetSerializerSettings()
{
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
DateFormatString = "yyyy-MM-ddTHH:mm:ssZ",
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
settings.Converters.Add(new StringEnumConverter());
return settings;
}
}
}

Просмотреть файл

@ -0,0 +1,45 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations;
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;
using Microsoft.Extensions.Logging;
namespace DurableFunctionsMonitor.DotNetBackend
{
// Base class for all HTTP request handlers.
// Provides tooling for authZ and error handling.
public abstract class HttpHandlerBase
{
// Default instance of IDurableClientFactory, injected via ctor
private readonly IDurableClientFactory _durableClientFactory;
public HttpHandlerBase(IDurableClientFactory durableClientFactory)
{
this._durableClientFactory = durableClientFactory;
}
// Applies authN/authZ rules and handles incoming HTTP request. Also creates IDurableClient (when needed) and does error handling.
protected async Task<IActionResult> HandleAuthAndErrors(IDurableClient defaultDurableClient, HttpRequest req, string connName, string hubName, ILogger log, Func<IDurableClient, Task<IActionResult>> todo)
{
return await Globals.HandleErrors(req, log, async () => {
await Auth.ValidateIdentityAsync(req.HttpContext.User, req.Headers, req.Cookies, Globals.CombineConnNameAndHubName(connName, hubName));
// For default storage connections using default durableClient, injected normally, as a parameter.
// Only using IDurableClientFactory for custom connections, just in case.
var durableClient = Globals.IsDefaultConnectionStringName(connName) ?
defaultDurableClient :
this._durableClientFactory.CreateClient(new DurableClientOptions
{
TaskHub = hubName,
ConnectionName = Globals.GetFullConnectionStringEnvVariableName(connName)
});
return await todo(durableClient);
});
}
}
}

Просмотреть файл

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.WindowsAzure.Storage.Table;
using Newtonsoft.Json.Linq;
namespace DurableFunctionsMonitor.DotNetBackend
{
static class OrchestrationHistory
{
/// <summary>
/// Fetches orchestration instance history directly from XXXHistory table
/// Tries to mimic this algorithm: https://github.com/Azure/azure-functions-durable-extension/blob/main/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs#L718
/// Intentionally returns IEnumerable<>, because the consuming code not always iterates through all of it.
/// </summary>
public static IEnumerable<HistoryEvent> GetHistoryDirectlyFromTable(IDurableClient durableClient, string connName, string hubName, string instanceId)
{
var tableClient = TableClient.GetTableClient(connName);
// Need to fetch executionId first
var instanceEntity = tableClient.ExecuteAsync($"{hubName}Instances", TableOperation.Retrieve(instanceId, string.Empty))
.Result.Result as DynamicTableEntity;
string executionId = instanceEntity.Properties.ContainsKey("ExecutionId") ?
instanceEntity.Properties["ExecutionId"].StringValue :
null;
var instanceIdFilter = TableQuery.CombineFilters
(
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, instanceId),
TableOperators.And,
TableQuery.GenerateFilterCondition("ExecutionId", QueryComparisons.Equal, executionId)
);
// Fetching _all_ correlated events with a separate parallel query. This seems to be the only option.
var correlatedEventsQuery = new TableQuery<HistoryEntity>().Where
(
TableQuery.CombineFilters
(
instanceIdFilter,
TableOperators.And,
TableQuery.GenerateFilterConditionForInt("TaskScheduledId", QueryComparisons.GreaterThanOrEqual, 0)
)
);
var correlatedEventsTask = tableClient.GetAllAsync($"{hubName}History", correlatedEventsQuery)
.ContinueWith(t => t.Result.ToDictionary(e => e.TaskScheduledId));
// Memorizing 'ExecutionStarted' event, to further correlate with 'ExecutionCompleted'
HistoryEntity executionStartedEvent = null;
// Fetching the history
var query = new TableQuery<HistoryEntity>().Where(instanceIdFilter);
foreach (var evt in tableClient.GetAll($"{hubName}History", query))
{
switch (evt.EventType)
{
case "TaskScheduled":
case "SubOrchestrationInstanceCreated":
// Trying to match the completion event
correlatedEventsTask.Result.TryGetValue(evt.EventId, out var correlatedEvt);
if (correlatedEvt != null)
{
yield return correlatedEvt.ToHistoryEvent
(
evt._Timestamp,
evt.Name,
correlatedEvt.EventType == "GenericEvent" ? evt.EventType : null,
evt.InstanceId
);
}
else
{
yield return evt.ToHistoryEvent();
}
break;
case "ExecutionStarted":
executionStartedEvent = evt;
yield return evt.ToHistoryEvent(null, evt.Name);
break;
case "ExecutionCompleted":
case "ExecutionFailed":
case "ExecutionTerminated":
yield return evt.ToHistoryEvent(executionStartedEvent?._Timestamp);
break;
case "ContinueAsNew":
case "TimerCreated":
case "TimerFired":
case "EventRaised":
case "EventSent":
yield return evt.ToHistoryEvent();
break;
}
}
}
private static HistoryEvent ToHistoryEvent(this HistoryEntity evt,
DateTimeOffset? scheduledTime = null,
string functionName = null,
string eventType = null,
string subOrchestrationId = null)
{
return new HistoryEvent
{
Timestamp = evt._Timestamp.ToUniversalTime(),
EventType = eventType ?? evt.EventType,
EventId = evt.TaskScheduledId,
Name = string.IsNullOrEmpty(evt.Name) ? functionName : evt.Name,
Result = evt.Result,
Details = evt.Details,
SubOrchestrationId = subOrchestrationId,
ScheduledTime = scheduledTime,
DurationInMs = scheduledTime.HasValue ? (evt._Timestamp - scheduledTime.Value).TotalMilliseconds : 0
};
}
internal static HistoryEvent ToHistoryEvent(JToken token)
{
dynamic dynamicToken = token;
return new HistoryEvent
{
Timestamp = dynamicToken.Timestamp,
EventType = dynamicToken.EventType,
EventId = dynamicToken.EventId,
Name = string.IsNullOrEmpty(dynamicToken.Name) ? dynamicToken.FunctionName : dynamicToken.Name,
ScheduledTime = dynamicToken.ScheduledTime,
Result = dynamicToken.Result?.ToString(),
Details = dynamicToken.Details?.ToString(),
DurationInMs = dynamicToken.DurationInMs,
SubOrchestrationId = dynamicToken.SubOrchestrationId
};
}
internal static IEnumerable<HistoryEvent> ApplyTimeFrom(this IEnumerable<HistoryEvent> events, DateTime? timeFrom)
{
if (timeFrom == null)
{
return events;
}
return events.Where(evt => evt.Timestamp >= timeFrom);
}
}
/// <summary>
/// Represents a record in orchestration's history
/// </summary>
public class HistoryEvent
{
public DateTimeOffset Timestamp { get; set; }
public string EventType { get; set; }
public int? EventId { get; set; }
public string Name { get; set; }
public DateTimeOffset? ScheduledTime { get; set; }
public string Result { get; set; }
public string Details { get; set; }
public double? DurationInMs { get; set; }
public string SubOrchestrationId { get; set; }
}
// Represents an record in XXXHistory table
class HistoryEntity : TableEntity
{
public string InstanceId { get; set; }
public string EventType { get; set; }
public string Name { get; set; }
public DateTimeOffset _Timestamp { get; set; }
public string Result { get; set; }
public string Details { get; set; }
public int EventId { get; set; }
public int? TaskScheduledId { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
namespace DurableFunctionsMonitor.DotNetBackend
{
/// <summary>
/// Defines functional mode for DurableFunctionsMonitor endpoint.
/// </summary>
public enum DfmMode
{
Normal = 0,
ReadOnly
}
/// <summary>
/// DurableFunctionsMonitor configuration settings
/// </summary>
public class DfmSettings
{
/// <summary>
/// Turns authentication off for DurableFunctionsMonitor endpoint.
/// WARNING: this might not only expose DurableFunctionsMonitor to the public, but also
/// expose all other HTTP-triggered endpoints in your project. Make sure you know what you're doing.
/// </summary>
public bool DisableAuthentication { get; set; }
/// <summary>
/// Functional mode for DurableFunctionsMonitor endpoint.
/// Currently only Normal (default) and ReadOnly modes are supported.
/// </summary>
public DfmMode Mode { get; set; }
/// <summary>
/// List of App Roles, that are allowed to access DurableFunctionsMonitor endpoint. Users/Groups then need
/// to be assigned one of these roles via AAD Enterprise Applications->[your AAD app]->Users and Groups tab.
/// Once set, the incoming access token is expected to contain one of these in its 'roles' claim.
/// </summary>
public IEnumerable<string> AllowedAppRoles { get; set; }
/// <summary>
/// List of users, that are allowed to access DurableFunctionsMonitor endpoint. You typically put emails into here.
/// Once set, the incoming access token is expected to contain one of these names in its 'preferred_username' claim.
/// </summary>
public IEnumerable<string> AllowedUserNames { get; set; }
/// <summary>
/// Folder where to search for custom tab/html templates.
/// Must be a part of your Functions project and be adjacent to your host.json file.
/// </summary>
public string CustomTemplatesFolderName { get; set; }
/// <summary>
/// Name of the claim (from ClaimsCredential) to be used as a user name.
/// Defaults to "preferred_username"
/// </summary>
public string UserNameClaimName { get; set; }
public DfmSettings()
{
this.UserNameClaimName = Auth.PreferredUserNameClaim;
}
}
/// <summary>
/// A set of extension points that can be customized by the client code, when DFM is used in 'injected' mode.
/// </summary>
public class DfmExtensionPoints
{
/// <summary>
/// Routine for fetching orchestration history.
/// Takes IDurableClient, connString env variable name, taskHubName and instanceId and returns IEnumerable[HistoryEvent].
/// Provide your own implementation for a custom storage provider.
/// Default implementation fetches history directly from XXXHistory table.
/// </summary>
public Func<IDurableClient, string, string, string, IEnumerable<HistoryEvent>> GetInstanceHistoryRoutine { get; set; }
public DfmExtensionPoints()
{
this.GetInstanceHistoryRoutine = OrchestrationHistory.GetHistoryDirectlyFromTable;
}
}
/// <summary>
/// DurableFunctionsMonitor configuration
/// </summary>
public static class DfmEndpoint
{
/// <summary>
/// Initializes DurableFunctionsMonitor endpoint with some settings
/// </summary>
/// <param name="settings">When null, default settings are used</param>
/// <param name="extensionPoints">Routines, that can be customized by client code. When null, default instance of DfmExtensionPoints is used</param>
public static void Setup(DfmSettings settings = null, DfmExtensionPoints extensionPoints = null)
{
string dfmNonce = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_NONCE);
_settings = settings;
if (_settings == null)
{
string dfmAllowedUserNames = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_ALLOWED_USER_NAMES);
string dfmAllowedAppRoles = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_ALLOWED_APP_ROLES);
string dfmMode = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_MODE);
string dfmUserNameClaimName = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_USERNAME_CLAIM_NAME);
_settings = new DfmSettings()
{
// Don't want to move the below initializatin to DfmSettings's ctor. The idea is: either _everything_ comes
// from env variables or _everything_ is configured programmatically. To avoid unclarity we shouldn't mix these two approaches.
DisableAuthentication = dfmNonce == Auth.ISureKnowWhatIAmDoingNonce,
Mode = dfmMode == DfmMode.ReadOnly.ToString() ? DfmMode.ReadOnly : DfmMode.Normal,
AllowedUserNames = dfmAllowedUserNames == null ? null : dfmAllowedUserNames.Split(','),
AllowedAppRoles = dfmAllowedAppRoles == null ? null : dfmAllowedAppRoles.Split(','),
UserNameClaimName = string.IsNullOrEmpty(dfmUserNameClaimName) ? Auth.PreferredUserNameClaim : dfmUserNameClaimName
};
}
if (extensionPoints != null)
{
_extensionPoints = extensionPoints;
}
// Also initializing CustomUserAgent value based on input parameters
if (!string.IsNullOrEmpty(dfmNonce) && (dfmNonce != Auth.ISureKnowWhatIAmDoingNonce))
{
_customUserAgent = $"DurableFunctionsMonitor-VsCodeExt/{GetVersion()}";
}
else
{
_customUserAgent = $"DurableFunctionsMonitor-Injected/{GetVersion()}";
}
}
internal static DfmSettings Settings
{
get
{
if (_settings != null)
{
return _settings;
}
if (!AreWeInStandaloneMode())
{
throw new InvalidOperationException("Make sure you called DfmEndpoint.Setup() in your code");
}
DfmEndpoint.Setup();
// Need to reinitialize CustomUserAgent
_customUserAgent = $"DurableFunctionsMonitor-Standalone/{GetVersion()}";
return _settings;
}
}
internal static DfmExtensionPoints ExtensionPoints
{
get { return _extensionPoints; }
}
internal static string CustomUserAgent
{
get { return _customUserAgent; }
}
private static DfmSettings _settings = null;
private static DfmExtensionPoints _extensionPoints = new DfmExtensionPoints();
private static string _customUserAgent;
/// <summary>
/// Checks whether we should do our internal initialization (Standalone mode)
/// or throw an exception when not initialized (Injected mode)
/// </summary>
private static bool AreWeInStandaloneMode()
{
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
if (string.IsNullOrEmpty(assemblyLocation))
{
return true;
}
string currentFolder = Path.GetDirectoryName(assemblyLocation);
string targetsFileName = "durablefunctionsmonitor.dotnetbackend.targets";
// Using our .targets file as a marker. It should only appear in our own output folder
return File.Exists(Path.Combine(currentFolder, targetsFileName)) ||
File.Exists(Path.Combine(Path.GetDirectoryName(currentFolder), targetsFileName));
}
private static string GetVersion()
{
var version = typeof(DfmEndpoint).Assembly.GetName().Version;
return $"{version.Major}.{version.Minor}.{version.Build}";
}
}
}

Просмотреть файл

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage;
namespace DurableFunctionsMonitor.DotNetBackend
{
// CloudTableClient wrapper interface. Seems to be the only way to unit-test.
public interface ITableClient
{
// Gets the list of table names
Task<IEnumerable<string>> ListTableNamesAsync();
// Synchronously retrieves all results from Azure Table
IEnumerable<TEntity> GetAll<TEntity>(string tableName, TableQuery<TEntity> query) where TEntity : TableEntity, new();
// Asynchronously retrieves all results from Azure Table
Task<IEnumerable<TEntity>> GetAllAsync<TEntity>(string tableName, TableQuery<TEntity> query) where TEntity : TableEntity, new();
// Executes a TableOperation
Task<TableResult> ExecuteAsync(string tableName, TableOperation operation);
}
// CloudTableClient wrapper. Seems to be the only way to unit-test.
class TableClient: ITableClient
{
// Cannot use DI functionality (our startup method will not be called when installed as a NuGet package),
// so just leaving this as an internal static variable.
internal static ITableClient MockedTableClient = null;
public static ITableClient GetTableClient(string connStringName)
{
if (MockedTableClient != null)
{
return MockedTableClient;
}
return new TableClient(connStringName);
}
private TableClient(string connStringName)
{
string connectionString = Environment.GetEnvironmentVariable(connStringName);
this._client = CloudStorageAccount.Parse(connectionString).CreateCloudTableClient();
}
/// <inheritdoc/>
public async Task<IEnumerable<string>> ListTableNamesAsync()
{
// Overriding User-Agent header
var operationContext = new OperationContext
{
CustomUserAgent = DfmEndpoint.CustomUserAgent
};
var result = new List<string>();
TableContinuationToken token = null;
do
{
var nextBatch = await this._client.ListTablesSegmentedAsync(null, null, token, null, operationContext);
result.AddRange(nextBatch.Results.Select(r => r.Name));
token = nextBatch.ContinuationToken;
}
while (token != null);
return result;
}
/// <inheritdoc/>
public IEnumerable<TEntity> GetAll<TEntity>(string tableName, TableQuery<TEntity> query)
where TEntity : TableEntity, new()
{
var table = this._client.GetTableReference(tableName);
// Overriding User-Agent header
var operationContext = new OperationContext
{
CustomUserAgent = DfmEndpoint.CustomUserAgent
};
TableContinuationToken token = null;
do
{
var nextBatch = table.ExecuteQuerySegmentedAsync(query, token, null, operationContext).Result;
foreach (var evt in nextBatch.Results)
{
yield return evt;
}
token = nextBatch.ContinuationToken;
}
while (token != null);
}
/// <inheritdoc/>
public async Task<IEnumerable<TEntity>> GetAllAsync<TEntity>(string tableName, TableQuery<TEntity> query)
where TEntity : TableEntity, new()
{
var table = this._client.GetTableReference(tableName);
// Overriding User-Agent header
var operationContext = new OperationContext
{
CustomUserAgent = DfmEndpoint.CustomUserAgent
};
var result = new List<TEntity>();
TableContinuationToken token = null;
do
{
var nextBatch = await table.ExecuteQuerySegmentedAsync(query, token, null, operationContext);
result.AddRange(nextBatch.Results);
token = nextBatch.ContinuationToken;
}
while (token != null);
return result;
}
/// <inheritdoc/>
public Task<TableResult> ExecuteAsync(string tableName, TableOperation operation)
{
// Overriding User-Agent header
var operationContext = new OperationContext
{
CustomUserAgent = DfmEndpoint.CustomUserAgent
};
return this._client.GetTableReference(tableName).ExecuteAsync(operation, null, operationContext);
}
private readonly CloudTableClient _client;
}
}

Двоичные данные
durablefunctionsmonitor.dotnetbackend/DfmStatics/favicon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.9 KiB

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by Microsoft Visio, SVG Export logo.svg Page-1 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"
width="3.94551in" height="3.69586in" viewBox="0 0 284.076 266.102" xml:space="preserve" color-interpolation-filters="sRGB"
class="st5">
<style type="text/css">
<![CDATA[
.st1 {fill:#3999c6;stroke:none;stroke-linecap:butt;stroke-width:9.5625}
.st2 {fill:#fcd116;stroke:none;stroke-linecap:butt;stroke-width:9.5625}
.st3 {fill:#ff8c00;fill-opacity:0.3;stroke:none;stroke-linecap:butt;stroke-width:9.5625}
.st4 {fill:#ff0000;stroke:none;stroke-linecap:butt;stroke-width:0.75}
.st5 {fill:none;fill-rule:evenodd;font-size:12px;overflow:visible;stroke-linecap:square;stroke-miterlimit:3}
]]>
</style>
<g>
<title>Page-1</title>
<g id="group1-1" transform="translate(26.8951,-27.5625)">
<title>Sheet.1</title>
<g id="group2-2">
<title>Sheet.2</title>
<g id="shape3-3" transform="translate(156.623,-41.1226)">
<title>Sheet.3</title>
<path d="M71.52 203.17 C73.66 201.02 73.31 197.09 71.52 194.94 L60.43 183.86 L11.09 135.94 C8.94 133.79 5.72
133.79 3.22 135.94 C1.07 138.09 0.36 142.02 3.22 144.16 L55.07 194.94 C57.21 197.09 57.21 201.02
55.07 203.17 L2.15 255.73 C0 257.88 0 261.81 2.15 263.96 C4.29 266.1 8.22 265.74 10.01 263.96 L59
215.32 C59 215.32 59 215.32 59.36 214.97 L71.52 203.17 Z" class="st1"/>
</g>
<g id="shape4-5" transform="translate(0,-41.1226)">
<title>Sheet.4</title>
<path d="M2.15 203.17 C0 201.02 0.36 197.09 2.15 194.94 L13.23 183.86 L62.58 135.94 C64.72 133.79 67.94 133.79
70.44 135.94 C72.59 138.09 73.31 142.02 70.44 144.16 L19.67 194.94 C17.52 197.09 17.52 201.02 19.67
203.17 L71.52 255.73 C73.66 257.88 73.66 261.81 71.52 263.96 C69.37 266.1 65.44 265.74 63.65 263.96
L13.59 216.04 C13.59 216.04 13.59 216.04 13.23 215.68 L2.15 203.17 Z" class="st1"/>
</g>
<g id="shape5-7" transform="translate(63.6506,0)">
<title>Sheet.5</title>
<path d="M107.28 55.12 L37.55 55.12 L0 160.97 L45.77 161.33 L10.01 266.1 L108.71 126.28 L60.79 126.28 L107.28
55.12 Z" class="st2"/>
</g>
<g id="shape6-9" transform="translate(73.663,0)">
<title>Sheet.6</title>
<path d="M50.78 126.28 L97.26 55.12 L60.79 55.12 L22.17 143.09 L67.94 143.45 L0 266.1 L98.69 126.28 L50.78 126.28
Z" class="st3"/>
</g>
</g>
</g>
<g id="shape20-11" transform="translate(77.9528,-80.2178)">
<title>Sheet.20</title>
<path d="M0 200.91 L18.19 200.91 L34.44 200.04 L46.78 155.72 C47.43 153.11 48.73 147.89 53.93 147.89 C59.13 147.89 59.78
153.11 59.78 155.72 L60.43 229.6 L69.52 204.39 C70.17 201.78 72.12 200.91 73.42 200.91 L126.05 200.91 C128.65
200.91 130.6 204.39 130.6 207 C130.6 209.61 128.65 214.82 126.05 214.82 L78.62 214.82 L61.73 260.02 C60.43
264.36 59.13 266.1 55.23 265.23 C53.28 264.36 51.33 260.89 51.33 258.28 L50.68 190.48 L45.48 208.74 C44.83
211.34 43.53 214.82 41.58 214.82 L25.99 214.82 L5.85 214.82 L2.6 207.87 L0 200.91 Z" class="st4"/>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 3.2 KiB

Просмотреть файл

@ -0,0 +1,15 @@
{
"short_name": "Durable Functions Monitor",
"name": "Durable Functions Monitor",
"icons": [
{
"src": "favicon.png",
"sizes": "64x64",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

Просмотреть файл

@ -0,0 +1,2 @@
.react-vis-magic-css-import-rule{display:inherit}.rv-treemap{font-size:12px;position:relative}.rv-treemap__leaf{overflow:hidden;position:absolute}.rv-treemap__leaf--circle{align-items:center;border-radius:100%;display:flex;justify-content:center}.rv-treemap__leaf__content{overflow:hidden;padding:10px;text-overflow:ellipsis}.rv-xy-plot{color:#c3c3c3;position:relative}.rv-xy-plot canvas{pointer-events:none}.rv-xy-plot .rv-xy-canvas{pointer-events:none;position:absolute}.rv-xy-plot__inner{display:block}.rv-xy-plot__axis__line{fill:none;stroke-width:2px;stroke:#e6e6e9}.rv-xy-plot__axis__tick__line{stroke:#e6e6e9}.rv-xy-plot__axis__tick__text,.rv-xy-plot__axis__title text{fill:#6b6b76;font-size:11px}.rv-xy-plot__grid-lines__line{stroke:#e6e6e9}.rv-xy-plot__circular-grid-lines__line{fill-opacity:0;stroke:#e6e6e9}.rv-xy-plot__series,.rv-xy-plot__series path{pointer-events:all}.rv-xy-plot__series--line{fill:none;stroke:#000;stroke-width:2px}.rv-crosshair{position:absolute;font-size:11px;pointer-events:none}.rv-crosshair__line{background:#47d3d9;width:1px}.rv-crosshair__inner{position:absolute;text-align:left;top:0}.rv-crosshair__inner__content{border-radius:4px;background:#3a3a48;color:#fff;font-size:12px;padding:7px 10px;box-shadow:0 2px 4px rgba(0,0,0,.5)}.rv-crosshair__inner--left{right:4px}.rv-crosshair__inner--right{left:4px}.rv-crosshair__title{font-weight:700;white-space:nowrap}.rv-crosshair__item{white-space:nowrap}.rv-hint{position:absolute;pointer-events:none}.rv-hint__content{border-radius:4px;padding:7px 10px;font-size:12px;background:#3a3a48;box-shadow:0 2px 4px rgba(0,0,0,.5);color:#fff;text-align:left;white-space:nowrap}.rv-discrete-color-legend{box-sizing:border-box;overflow-y:auto;font-size:12px}.rv-discrete-color-legend.horizontal{white-space:nowrap}.rv-discrete-color-legend-item{color:#3a3a48;border-radius:1px;padding:9px 10px}.rv-discrete-color-legend-item.horizontal{display:inline-block}.rv-discrete-color-legend-item.horizontal .rv-discrete-color-legend-item__title{margin-left:0;display:block}.rv-discrete-color-legend-item__color{display:inline-block;vertical-align:middle;overflow:visible}.rv-discrete-color-legend-item__color__path{stroke:#dcdcdc;stroke-width:2px}.rv-discrete-color-legend-item__title{margin-left:10px}.rv-discrete-color-legend-item.disabled{color:#b8b8b8}.rv-discrete-color-legend-item.clickable{cursor:pointer}.rv-discrete-color-legend-item.clickable:hover{background:#f9f9f9}.rv-search-wrapper{display:flex;flex-direction:column}.rv-search-wrapper__form{flex:0 1}.rv-search-wrapper__form__input{width:100%;color:#a6a6a5;border:1px solid #e5e5e4;padding:7px 10px;font-size:12px;box-sizing:border-box;border-radius:2px;margin:0 0 9px;outline:0}.rv-search-wrapper__contents{flex:1 1;overflow:auto}.rv-continuous-color-legend{font-size:12px}.rv-continuous-color-legend .rv-gradient{height:4px;border-radius:2px;margin-bottom:5px}.rv-continuous-size-legend{font-size:12px}.rv-continuous-size-legend .rv-bubbles{text-align:justify;overflow:hidden;margin-bottom:5px;width:100%}.rv-continuous-size-legend .rv-bubble{background:#d8d9dc;display:inline-block;vertical-align:bottom}.rv-continuous-size-legend .rv-spacer{display:inline-block;font-size:0;line-height:0;width:100%}.rv-legend-titles{height:16px;position:relative}.rv-legend-titles__center,.rv-legend-titles__left,.rv-legend-titles__right{position:absolute;white-space:nowrap;overflow:hidden}.rv-legend-titles__center{display:block;text-align:center;width:100%}.rv-legend-titles__right{right:0}.rv-radial-chart .rv-xy-plot__series--label{pointer-events:none}
/*# sourceMappingURL=2.62e7949a.chunk.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,2 @@
html{overflow-y:scroll!important}body{margin:0;padding:0;font-family:sans-serif;display:table;width:100%}.top-appbar{box-shadow:none!important}.top-toolbar{padding-top:5px;padding-bottom:18px;max-width:100vw;min-width:900px}.toolbar-select{min-width:150px}.long-text-cell{min-width:160px;max-width:250px}.long-text-cell-input{font-size:x-small!important}.long-text-cell-input:hover{text-decoration:underline;cursor:pointer;opacity:.8}.empty-table-placeholder{padding:30px;text-align:center}.name-cell{min-width:165px;word-break:break-all}.link-with-pointer-cursor{cursor:pointer}.time-zone-name-span{padding-left:2px;font-size:x-small}.title-typography{padding-right:10px}.app-bar{margin-bottom:10px}.instance-id-input{width:320px}.instance-id-valid .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline{border-color:green!important}.raw-html-div{padding:10px}.login-progress{text-align:center;margin-top:20px;margin-bottom:20px}.task-hub-list{padding-bottom:25px!important}.show-time-as-typography{padding-top:10px;padding-left:16px;padding-right:10px}.datetime-cell{min-width:145px}.till-checkbox{padding:20px 14px 4px 4px!important}.till-label{padding-left:15px!important}.from-input{width:215px;margin-left:10px!important}.till-input{width:215px}.time-period-menu-drop-btn{width:32px!important;min-width:32px!important;margin-top:16px!important}.filter-value-input{flex:1 1;margin-left:20px!important}.filtered-column-input{width:180px}.items-count-label{margin-top:8px!important;padding-left:15px;height:20px}.instance-id-cell{min-width:145px;max-width:270px;word-break:break-all}.output-cell{min-width:75px;max-width:200px}.entity-type-radio{margin-bottom:0}.toolbar-grid1{width:260px!important}.toolbar-grid1-item2{padding-top:20px;min-height:68px!important;min-width:257px!important}.toolbar-grid2{margin-left:40px;margin-right:40px}.toolbar-grid2-item-1{display:flex}.toolbar-grid2-item1-select{margin-left:20px!important}.toolbar-grid2-item2{padding-top:20px}.toolbar-grid3{width:auto!important}.toolbar-grid3-item2{padding-top:26px}.toolbar-runtime-status-group{margin-left:30px;margin-right:30px}.toolbar-runtime-status-group-label{padding-left:10px!important}.form-control-float-right{float:right}.column-hide-button{width:8px!important;padding:0!important;margin-right:-8px!important;float:right;opacity:.7}.unhide-button{vertical-align:text-bottom!important;font-size:12px}.tab-buttons{min-height:37px!important}.histogram-legend{padding-left:70px;white-space:normal!important}.histogram-legend-dark-mode>div>span{color:#d3d3d3}.status-checkbox{padding-left:10px!important;padding-top:5px!important;padding-bottom:5px!important}.autorefresh-select{min-width:130px}.refresh-button{width:130px}.selected-statuses-box{display:flex;flex-wrap:wrap}.message-snackbar{top:80px!important}.error-icon{margin-right:10px;margin-bottom:-7px}.error-snackbar-content{background-color:red!important}.settings-group{padding-top:5px;padding-left:20px;padding-bottom:5px}.link-to-az-func-as-a-graph{padding-top:10px}.metrics-chip{margin-right:3px;font-size:xx-small!important}.metrics-span{position:absolute;visibility:hidden;white-space:nowrap}.total-metrics-span{display:inherit;padding-top:9px;padding-left:20px}.diagram-div{padding-top:30px;padding-bottom:30px}.diagram-div>svg{display:block;margin:auto;width:100%!important;height:100%!important}.link-to-az-func-as-a-graph{float:right;padding-right:10px}.details-top-toolbar{padding-top:5px;padding-bottom:20px;max-width:100vw;min-width:900px}.grid-container{padding-top:20px;padding-right:15px}.grid-item{padding-left:15px}.details-datetime-cell{min-width:250px}.history-events-count-label{padding-top:10px;padding-left:16px;padding-bottom:10px}.details-refresh-button{width:90px}.functions-graph-tab-span{display:inline-flex;flex-direction:row}.functions-graph-link-icon{width:15px!important;margin-top:-2px;padding-left:5px}.history-filtered-column-input{width:150px}.history-filter-value-input{width:250px}.history-appbar{margin-top:15px;margin-bottom:15px;padding-top:5px;padding-bottom:5px}.history-toolbar{margin-left:-8px!important;padding-left:0!important;padding-top:17px!important}.history-from-input{width:210px}.history-from-checkbox{padding-top:18px!important}.history-from-label{margin-left:-40px}.purge-history-from-input,.purge-history-till-input{padding-bottom:20px!important}.purge-history-till-input{margin-left:20px!important}.purge-history-apply-to{padding-top:10px!important;padding-bottom:10px!important}.success-message{color:green!important}.dialog-text-field{padding-bottom:10px}
/*# sourceMappingURL=main.12374d2f.chunk.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,855 @@
/*
* __ ___
* _____/ /___ __/ (_)____
* / ___/ __/ / / / / / ___/
* (__ ) /_/ /_/ / / (__ )
* /____/\__/\__, /_/_/____/
* /____/
*
* light - weight css preprocessor @licence MIT
*/
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
* Wait for document loaded before starting the execution
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/*! ../../config */
/*! ../../dagre-wrapper/index.js */
/*! ../../logger */
/*! ../../mermaidAPI */
/*! ../../utils */
/*! ../common/common */
/*! ../config */
/*! ../createLabel */
/*! ../diagrams/class/svgDraw */
/*! ../intersect/index.js */
/*! ../logger */
/*! ../package.json */
/*! ../utils */
/*! ./../../../../node_modules/process/browser.js */
/*! ./../../../../node_modules/webpack/buildin/module.js */
/*! ./../process/browser.js */
/*! ./classDb */
/*! ./clusters */
/*! ./config */
/*! ./createLabel */
/*! ./defaultConfig */
/*! ./diagrams/class/classDb */
/*! ./diagrams/class/classRenderer */
/*! ./diagrams/class/classRenderer-v2 */
/*! ./diagrams/class/parser/classDiagram */
/*! ./diagrams/class/styles */
/*! ./diagrams/common/common */
/*! ./diagrams/er/erDb */
/*! ./diagrams/er/erRenderer */
/*! ./diagrams/er/parser/erDiagram */
/*! ./diagrams/er/styles */
/*! ./diagrams/flowchart/flowDb */
/*! ./diagrams/flowchart/flowRenderer */
/*! ./diagrams/flowchart/flowRenderer-v2 */
/*! ./diagrams/flowchart/parser/flow */
/*! ./diagrams/flowchart/styles */
/*! ./diagrams/gantt/ganttDb */
/*! ./diagrams/gantt/ganttRenderer */
/*! ./diagrams/gantt/parser/gantt */
/*! ./diagrams/gantt/styles */
/*! ./diagrams/git/gitGraphAst */
/*! ./diagrams/git/gitGraphRenderer */
/*! ./diagrams/git/parser/gitGraph */
/*! ./diagrams/git/styles */
/*! ./diagrams/info/infoDb */
/*! ./diagrams/info/infoRenderer */
/*! ./diagrams/info/parser/info */
/*! ./diagrams/info/styles */
/*! ./diagrams/pie/parser/pie */
/*! ./diagrams/pie/pieDb */
/*! ./diagrams/pie/pieRenderer */
/*! ./diagrams/pie/styles */
/*! ./diagrams/sequence/parser/sequenceDiagram */
/*! ./diagrams/sequence/sequenceDb */
/*! ./diagrams/sequence/sequenceRenderer */
/*! ./diagrams/sequence/styles */
/*! ./diagrams/state/parser/stateDiagram */
/*! ./diagrams/state/stateDb */
/*! ./diagrams/state/stateRenderer */
/*! ./diagrams/state/stateRenderer-v2 */
/*! ./diagrams/state/styles */
/*! ./diagrams/user-journey/journeyDb */
/*! ./diagrams/user-journey/journeyRenderer */
/*! ./diagrams/user-journey/parser/journey */
/*! ./diagrams/user-journey/styles */
/*! ./edges */
/*! ./erDb */
/*! ./erMarkers */
/*! ./errorRenderer */
/*! ./flowChartShapes */
/*! ./flowDb */
/*! ./ganttDb */
/*! ./gitGraphAst */
/*! ./id-cache.js */
/*! ./infoDb */
/*! ./intersect-circle.js */
/*! ./intersect-ellipse */
/*! ./intersect-ellipse.js */
/*! ./intersect-line */
/*! ./intersect-node.js */
/*! ./intersect-polygon.js */
/*! ./intersect-rect.js */
/*! ./intersect/index.js */
/*! ./intersect/intersect-rect */
/*! ./journeyDb */
/*! ./logger */
/*! ./markers */
/*! ./mermaid-graphlib */
/*! ./mermaidAPI */
/*! ./nodes */
/*! ./parser/classDiagram */
/*! ./parser/erDiagram */
/*! ./parser/flow */
/*! ./parser/gantt */
/*! ./parser/gitGraph */
/*! ./parser/info */
/*! ./parser/journey */
/*! ./parser/pie */
/*! ./parser/sequenceDiagram */
/*! ./parser/stateDiagram */
/*! ./pieDb */
/*! ./sequenceDb */
/*! ./shapes */
/*! ./shapes/note */
/*! ./shapes/util */
/*! ./stateDb */
/*! ./styles */
/*! ./svgDraw */
/*! ./theme-base */
/*! ./theme-dark */
/*! ./theme-default */
/*! ./theme-forest */
/*! ./theme-helpers */
/*! ./theme-neutral */
/*! ./themes */
/*! ./util */
/*! ./utils */
/*! @braintree/sanitize-url */
/*! Check if previously processed */
/*! d3 */
/*! dagre */
/*! dagre-d3 */
/*! dagre-d3/lib/label/add-html-label.js */
/*! entity-decode/browser */
/*! exports provided: LEVELS, logger, setLogLevel */
/*! exports provided: addClasses, addRelations, setConf, drawOld, draw, default */
/*! exports provided: addToRender, addToRenderV2, default */
/*! exports provided: bounds, drawActors, setConf, draw, default */
/*! exports provided: calcThemeVariables, default */
/*! exports provided: clear, insertEdgeLabel, positionEdgeLabel, intersection, insertEdge */
/*! exports provided: clusterDb, clear, extractDecendants, validate, findNonClusterChild, adjustClustersAndEdges, extractor, sortNodesByHierarchy */
/*! exports provided: default */
/*! exports provided: defaultConfig, updateCurrentConfig, setSiteConfig, setSiteConfigDelta, updateSiteConfig, getSiteConfig, setConfig, getConfig, sanitize, addDirective, reset */
/*! exports provided: detectInit, detectDirective, detectType, isSubstringInArray, interpolateToCurve, formatUrl, runFunc, getStylesFromArray, generateId, random, assignWithDepth, getTextObj, drawSimpleText, wrapLabel, calculateTextHeight, calculateTextWidth, calculateTextDimensions, calculateSvgSizeAttrs, configureSvgSize, default */
/*! exports provided: drawEdge, drawClass, parseMember, default */
/*! exports provided: drawRect, drawFace, drawCircle, drawText, drawLabel, drawSection, drawTask, drawBackgroundRect, getTextObj, getNoteRect, default */
/*! exports provided: drawRect, drawText, drawLabel, drawActor, anchorElement, drawActivation, drawLoop, drawBackgroundRect, insertArrowHead, insertSequenceNumber, insertArrowCrossHead, getTextObj, getNoteRect, default */
/*! exports provided: drawStartState, drawDivider, drawSimpleState, drawDescrState, addTitleAndBox, drawText, drawNote, drawState, drawEdge */
/*! exports provided: encodeEntities, decodeEntities, default */
/*! exports provided: getRows, removeScript, sanitizeText, lineBreakRegex, hasBreaks, splitBreaks, default */
/*! exports provided: getThemeVariables */
/*! exports provided: insertCluster, getClusterTitleWidth, clear, positionCluster */
/*! exports provided: insertNode, setNodeElem, clear, positionNode */
/*! exports provided: labelHelper, updateNodeBounds, insertPolygonShape */
/*! exports provided: mkBorder */
/*! exports provided: name, version, description, main, keywords, scripts, repository, author, license, standard, dependencies, devDependencies, files, yarn-upgrade-all, sideEffects, husky, default */
/*! exports provided: parseDirective, addActor, addMessage, addSignal, getMessages, getActors, getActor, getActorKeys, getTitle, getTitleWrapped, enableSequenceNumbers, showSequenceNumbers, setWrap, autoWrap, clear, parseMessage, LINETYPE, ARROWTYPE, PLACEMENT, addNote, setTitle, apply, default */
/*! exports provided: parseDirective, addClass, lookUpDomId, clear, getClass, getClasses, getRelations, addRelation, addAnnotation, addMember, addMembers, cleanupLabel, setCssClass, setLink, setClickEvent, bindFunctions, lineType, relationType, default */
/*! exports provided: parseDirective, addState, clear, getState, getStates, logDocuments, getRelations, addRelation, cleanupLabel, lineType, relationType, default */
/*! exports provided: parseDirective, clear, setAxisFormat, getAxisFormat, setTodayMarker, getTodayMarker, setDateFormat, enableInclusiveEndDates, endDatesAreInclusive, getDateFormat, setExcludes, getExcludes, setTitle, getTitle, addSection, getSections, getTasks, addTask, findTaskById, addTaskOrg, setLink, setClass, setClickEvent, bindFunctions, default */
/*! exports provided: parseDirective, clear, setTitle, getTitle, addSection, getSections, getTasks, addTask, addTaskOrg, default */
/*! exports provided: parseDirective, default */
/*! exports provided: parseDirective, lookUpDomId, addVertex, addSingleLink, addLink, updateLinkInterpolate, updateLink, addClass, setDirection, setClass, setLink, getTooltip, setClickEvent, bindFunctions, getDirection, getVertices, getEdges, getClasses, clear, setGen, defaultStyle, addSubGraph, getDepthFirstPos, indexNodes, getSubGraphs, firstGraph, default */
/*! exports provided: render */
/*! exports provided: set, get, keys, size, default */
/*! exports provided: setConf, addVertices, addEdges, getClasses, draw, default */
/*! exports provided: setConf, draw, bounds, drawTasks, default */
/*! exports provided: setConf, draw, default */
/*! exports provided: setConf, getClasses, draw, default */
/*! exports provided: setDirection, setOptions, getOptions, commit, branch, merge, checkout, reset, prettyPrint, clear, getBranchesAsObjArray, getBranches, getCommits, getCommitsArray, getCurrentBranch, getDirection, getHead, default */
/*! exports provided: setMessage, getMessage, setInfo, getInfo, default */
/*! fs */
/*! graphlib */
/*! khroma */
/*! moment-mini */
/*! no static exports found */
/*! path */
/*! sequence config was passed as #1 */
/*! stylis */
/*!*********************!*\
!*** external "d3" ***!
\*********************/
/*!**********************!*\
!*** ./package.json ***!
\**********************/
/*!**********************!*\
!*** ./src/utils.js ***!
\**********************/
/*!***********************!*\
!*** ./src/config.js ***!
\***********************/
/*!***********************!*\
!*** ./src/logger.js ***!
\***********************/
/*!***********************!*\
!*** ./src/styles.js ***!
\***********************/
/*!************************!*\
!*** ./src/mermaid.js ***!
\************************/
/*!************************!*\
!*** external "dagre" ***!
\************************/
/*!*************************!*\
!*** external "khroma" ***!
\*************************/
/*!*************************!*\
!*** external "stylis" ***!
\*************************/
/*!***************************!*\
!*** ./src/mermaidAPI.js ***!
\***************************/
/*!***************************!*\
!*** external "dagre-d3" ***!
\***************************/
/*!***************************!*\
!*** external "graphlib" ***!
\***************************/
/*!*****************************!*\
!*** ./src/themes/index.js ***!
\*****************************/
/*!******************************!*\
!*** ./src/defaultConfig.js ***!
\******************************/
/*!******************************!*\
!*** ./src/errorRenderer.js ***!
\******************************/
/*!******************************!*\
!*** external "moment-mini" ***!
\******************************/
/*!*********************************!*\
!*** ./src/diagrams/er/erDb.js ***!
\*********************************/
/*!**********************************!*\
!*** ./src/themes/theme-base.js ***!
\**********************************/
/*!**********************************!*\
!*** ./src/themes/theme-dark.js ***!
\**********************************/
/*!***********************************!*\
!*** (webpack)/buildin/module.js ***!
\***********************************/
/*!***********************************!*\
!*** ./src/diagrams/er/styles.js ***!
\***********************************/
/*!***********************************!*\
!*** ./src/diagrams/pie/pieDb.js ***!
\***********************************/
/*!************************************!*\
!*** ./src/dagre-wrapper/edges.js ***!
\************************************/
/*!************************************!*\
!*** ./src/dagre-wrapper/index.js ***!
\************************************/
/*!************************************!*\
!*** ./src/dagre-wrapper/nodes.js ***!
\************************************/
/*!************************************!*\
!*** ./src/diagrams/git/styles.js ***!
\************************************/
/*!************************************!*\
!*** ./src/diagrams/pie/styles.js ***!
\************************************/
/*!************************************!*\
!*** ./src/themes/theme-forest.js ***!
\************************************/
/*!*************************************!*\
!*** ./src/diagrams/info/infoDb.js ***!
\*************************************/
/*!*************************************!*\
!*** ./src/diagrams/info/styles.js ***!
\*************************************/
/*!*************************************!*\
!*** ./src/themes/theme-default.js ***!
\*************************************/
/*!*************************************!*\
!*** ./src/themes/theme-helpers.js ***!
\*************************************/
/*!*************************************!*\
!*** ./src/themes/theme-neutral.js ***!
\*************************************/
/*!**************************************!*\
!*** ./src/dagre-wrapper/markers.js ***!
\**************************************/
/*!**************************************!*\
!*** ./src/diagrams/class/styles.js ***!
\**************************************/
/*!**************************************!*\
!*** ./src/diagrams/er/erMarkers.js ***!
\**************************************/
/*!**************************************!*\
!*** ./src/diagrams/gantt/styles.js ***!
\**************************************/
/*!**************************************!*\
!*** ./src/diagrams/state/shapes.js ***!
\**************************************/
/*!**************************************!*\
!*** ./src/diagrams/state/styles.js ***!
\**************************************/
/*!***************************************!*\
!*** ./src/dagre-wrapper/clusters.js ***!
\***************************************/
/*!***************************************!*\
!*** ./src/diagrams/class/classDb.js ***!
\***************************************/
/*!***************************************!*\
!*** ./src/diagrams/class/svgDraw.js ***!
\***************************************/
/*!***************************************!*\
!*** ./src/diagrams/common/common.js ***!
\***************************************/
/*!***************************************!*\
!*** ./src/diagrams/er/erRenderer.js ***!
\***************************************/
/*!***************************************!*\
!*** ./src/diagrams/gantt/ganttDb.js ***!
\***************************************/
/*!***************************************!*\
!*** ./src/diagrams/state/stateDb.js ***!
\***************************************/
/*!****************************************!*\
!*** ./src/diagrams/state/id-cache.js ***!
\****************************************/
/*!****************************************!*\
!*** external "entity-decode/browser" ***!
\****************************************/
/*!*****************************************!*\
!*** ./node_modules/process/browser.js ***!
\*****************************************/
/*!*****************************************!*\
!*** ./src/diagrams/git/gitGraphAst.js ***!
\*****************************************/
/*!*****************************************!*\
!*** ./src/diagrams/pie/pieRenderer.js ***!
\*****************************************/
/*!*****************************************!*\
!*** ./src/diagrams/sequence/styles.js ***!
\*****************************************/
/*!******************************************!*\
!*** ./src/dagre-wrapper/createLabel.js ***!
\******************************************/
/*!******************************************!*\
!*** ./src/dagre-wrapper/shapes/note.js ***!
\******************************************/
/*!******************************************!*\
!*** ./src/dagre-wrapper/shapes/util.js ***!
\******************************************/
/*!******************************************!*\
!*** ./src/diagrams/flowchart/flowDb.js ***!
\******************************************/
/*!******************************************!*\
!*** ./src/diagrams/flowchart/styles.js ***!
\******************************************/
/*!******************************************!*\
!*** ./src/diagrams/sequence/svgDraw.js ***!
\******************************************/
/*!******************************************!*\
!*** external "@braintree/sanitize-url" ***!
\******************************************/
/*!*******************************************!*\
!*** ./src/diagrams/info/infoRenderer.js ***!
\*******************************************/
/*!*******************************************!*\
!*** ./src/diagrams/pie/parser/pie.jison ***!
\*******************************************/
/*!*********************************************!*\
!*** ./src/diagrams/class/classRenderer.js ***!
\*********************************************/
/*!*********************************************!*\
!*** ./src/diagrams/gantt/ganttRenderer.js ***!
\*********************************************/
/*!*********************************************!*\
!*** ./src/diagrams/info/parser/info.jison ***!
\*********************************************/
/*!*********************************************!*\
!*** ./src/diagrams/sequence/sequenceDb.js ***!
\*********************************************/
/*!*********************************************!*\
!*** ./src/diagrams/state/stateRenderer.js ***!
\*********************************************/
/*!*********************************************!*\
!*** ./src/diagrams/user-journey/styles.js ***!
\*********************************************/
/*!**********************************************!*\
!*** ./src/dagre-wrapper/intersect/index.js ***!
\**********************************************/
/*!**********************************************!*\
!*** ./src/diagrams/git/gitGraphRenderer.js ***!
\**********************************************/
/*!**********************************************!*\
!*** ./src/diagrams/user-journey/svgDraw.js ***!
\**********************************************/
/*!***********************************************!*\
!*** ./node_modules/path-browserify/index.js ***!
\***********************************************/
/*!***********************************************!*\
!*** ./src/dagre-wrapper/mermaid-graphlib.js ***!
\***********************************************/
/*!***********************************************!*\
!*** ./src/diagrams/gantt/parser/gantt.jison ***!
\***********************************************/
/*!************************************************!*\
!*** ./src/diagrams/class/classRenderer-v2.js ***!
\************************************************/
/*!************************************************!*\
!*** ./src/diagrams/er/parser/erDiagram.jison ***!
\************************************************/
/*!************************************************!*\
!*** ./src/diagrams/flowchart/flowRenderer.js ***!
\************************************************/
/*!************************************************!*\
!*** ./src/diagrams/git/parser/gitGraph.jison ***!
\************************************************/
/*!************************************************!*\
!*** ./src/diagrams/state/stateRenderer-v2.js ***!
\************************************************/
/*!************************************************!*\
!*** ./src/diagrams/user-journey/journeyDb.js ***!
\************************************************/
/*!**************************************************!*\
!*** ./src/diagrams/flowchart/parser/flow.jison ***!
\**************************************************/
/*!***************************************************!*\
!*** ./src/diagrams/flowchart/flowChartShapes.js ***!
\***************************************************/
/*!***************************************************!*\
!*** ./src/diagrams/flowchart/flowRenderer-v2.js ***!
\***************************************************/
/*!***************************************************!*\
!*** ./src/diagrams/sequence/sequenceRenderer.js ***!
\***************************************************/
/*!******************************************************!*\
!*** ./node_modules/node-libs-browser/mock/empty.js ***!
\******************************************************/
/*!******************************************************!*\
!*** ./src/diagrams/class/parser/classDiagram.jison ***!
\******************************************************/
/*!******************************************************!*\
!*** ./src/diagrams/state/parser/stateDiagram.jison ***!
\******************************************************/
/*!******************************************************!*\
!*** ./src/diagrams/user-journey/journeyRenderer.js ***!
\******************************************************/
/*!*******************************************************!*\
!*** ./src/dagre-wrapper/intersect/intersect-line.js ***!
\*******************************************************/
/*!*******************************************************!*\
!*** ./src/dagre-wrapper/intersect/intersect-node.js ***!
\*******************************************************/
/*!*******************************************************!*\
!*** ./src/dagre-wrapper/intersect/intersect-rect.js ***!
\*******************************************************/
/*!*******************************************************!*\
!*** external "dagre-d3/lib/label/add-html-label.js" ***!
\*******************************************************/
/*!********************************************************!*\
!*** ./src/diagrams/user-journey/parser/journey.jison ***!
\********************************************************/
/*!*********************************************************!*\
!*** ./src/dagre-wrapper/intersect/intersect-circle.js ***!
\*********************************************************/
/*!**********************************************************!*\
!*** ./src/dagre-wrapper/intersect/intersect-ellipse.js ***!
\**********************************************************/
/*!**********************************************************!*\
!*** ./src/dagre-wrapper/intersect/intersect-polygon.js ***!
\**********************************************************/
/*!************************************************************!*\
!*** ./src/diagrams/sequence/parser/sequenceDiagram.jison ***!
\************************************************************/
/**
* @license
* Copyright (c) 2012-2013 Chris Pettitt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A better abstraction over CSS.
*
* @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present
* @website https://github.com/cssinjs/jss
* @license MIT
*/
/** @license React v0.18.0
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.12.0
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.12.0
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.12.0
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**!
* @fileOverview Kickass library to create and place poppers near their reference elements.
* @version 1.16.1-lts
* @license
* Copyright (c) 2016 Federico Zivolo and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
//! moment.js

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше