├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── devskim.yml │ └── gh-sync.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── NOTICE.txt ├── README.md ├── SECURITY.md ├── ci ├── ci.yml ├── signed.yml ├── steps-build-core.yml ├── steps-build-samples.yml ├── steps-sdl-tools.yml └── steps-sign.yml ├── docfx.json ├── docs ├── .gitignore ├── articles │ ├── intro.md │ └── toc.yml ├── index.md └── toc.yml ├── examples ├── echo-kernel │ ├── Echo Kernel.ipynb │ ├── EchoEngine.cs │ ├── KernelProperties.cs │ ├── Program.cs │ ├── README.md │ ├── echo-kernel.csproj │ └── res │ │ └── logo-64x64.png └── moon-kernel │ ├── DynValueConverter.cs │ ├── KernelProperties.cs │ ├── MoonEngine.cs │ ├── MoonScript Kernel.ipynb │ ├── Program.cs │ └── moon-kernel.csproj ├── jupyter-core.sln ├── src ├── Data │ ├── ConnectionInfo.cs │ ├── Enumerations.cs │ ├── KernelContext.cs │ ├── Metadata.cs │ ├── Protocol.cs │ └── Serialization.cs ├── Engines │ ├── BaseEngine.cs │ ├── CompleteRequestHandler.cs │ ├── CompletionResult.cs │ ├── ExecuteRequestHandler.cs │ ├── ExecutionResult.cs │ ├── IChannel.cs │ ├── IExecutionEngine.cs │ └── InputParser.cs ├── Extensions │ ├── ChannelWithNewLines.cs │ ├── Collections.cs │ ├── Conversions.cs │ ├── Cryptography.cs │ ├── Extensions.cs │ ├── Messages.cs │ └── ServiceCollection.cs ├── KernelApplication.cs ├── Properties │ ├── 267DevDivSNKey2048.snk │ ├── AssemblyInfo.cs │ └── DelaySign.cs ├── ResultEncoding │ ├── BasicEncoders.cs │ ├── DisplayData.cs │ ├── SymbolEncoder.cs │ └── Table.cs ├── Servers │ ├── HeartbeatServer.cs │ ├── IHeartbeatServer.cs │ ├── IShellServer.cs │ └── ShellServer.cs ├── ShellRouting │ ├── CommsRouter.cs │ ├── ICommsRouter.cs │ ├── IShellHandler.cs │ ├── IShellRouter.cs │ ├── OrderedShellHandler.cs │ └── ShellRouter.cs ├── Symbols │ ├── ISymbolResolver.cs │ └── Magic.cs └── jupyter-core.csproj ├── tests ├── core │ ├── EncoderTests.cs │ ├── InputParserTests.cs │ ├── Properties │ │ └── 267DevDivSNKey2048.snk │ └── core.csproj └── protocol │ ├── test_iecho.py │ └── test_imoon.py └── tutorial.md /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | "ms-dotnettools.csharp" 4 | ], 5 | "features": { 6 | "ghcr.io/devcontainers/features/dotnet:1": { 7 | "version": "3.1.423" 8 | }, 9 | "ghcr.io/devcontainers/features/python:1": { 10 | "version": "3.8" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/devskim.yml: -------------------------------------------------------------------------------- 1 | name: DevSkim 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | schedule: 10 | # set schedule to run at 2AM PT on Saturdays 11 | - cron: '0 9 * * Sat' 12 | 13 | jobs: 14 | lint: 15 | name: DevSkim 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Run DevSkim scanner 26 | uses: microsoft/DevSkim-Action@v1 27 | 28 | - name: Upload DevSkim scan results to GitHub Security tab 29 | uses: github/codeql-action/upload-sarif@v2 30 | with: 31 | sarif_file: devskim-results.sarif 32 | -------------------------------------------------------------------------------- /.github/workflows/gh-sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync GitHub with ADO 2 | 3 | on: 4 | issues: 5 | types: [closed, edited, deleted, reopened, assigned, unassigned, labeled, unlabeled] 6 | issue_comment: 7 | 8 | concurrency: 9 | group: ${{ github.event.issue.number }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | sync-issues: 14 | name: Run gh-sync from GitHub action 15 | if: ${{ github.event.label.name == 'tracking' || contains(github.event.issue.labels.*.name, 'tracking') }} 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Login to Azure 19 | uses: Azure/login@v1 20 | with: 21 | creds: ${{ secrets.AZURE_CREDENTIALS }} 22 | 23 | - id: AzureKeyVault 24 | uses: Azure/get-keyvault-secrets@v1 25 | with: 26 | keyvault: 'kv-qdk-build' 27 | secrets: 'ghSyncBuildPAT' 28 | 29 | - name: 'Trigger gh-sync' 30 | uses: microsoft/gh-sync@main 31 | with: 32 | ado-organization-url: ${{ secrets.ADO_URL }} 33 | ado-project: ${{ secrets.ADO_PROJECT }} 34 | ado-area-path: ${{ secrets.ADO_AREA_PATH }} 35 | github-repo: 'microsoft/jupyter-core' 36 | issue-number: ${{ github.event.issue.number }} 37 | ado-token: ${{ steps.AzureKeyVault.outputs.ghSyncBuildPAT }} 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/ 2 | ## Ignore Visual Studio temporary files, build results, and 3 | ## files generated by popular Visual Studio add-ons. 4 | ## 5 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 6 | 7 | # User-specific files 8 | *.rsuser 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | [Aa][Rr][Mm]/ 25 | [Aa][Rr][Mm]64/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | [Ll]og/ 30 | 31 | # Visual Studio 2015/2017 cache/options directory 32 | .vs/ 33 | # Uncomment if you have tasks that create the project's static files in wwwroot 34 | #wwwroot/ 35 | 36 | # Visual Studio 2017 auto generated files 37 | Generated\ Files/ 38 | 39 | # MSTest test Results 40 | [Tt]est[Rr]esult*/ 41 | [Bb]uild[Ll]og.* 42 | 43 | # NUNIT 44 | *.VisualState.xml 45 | TestResult.xml 46 | 47 | # Build Results of an ATL Project 48 | [Dd]ebugPS/ 49 | [Rr]eleasePS/ 50 | dlldata.c 51 | 52 | # Benchmark Results 53 | BenchmarkDotNet.Artifacts/ 54 | 55 | # .NET Core 56 | project.lock.json 57 | project.fragment.lock.json 58 | artifacts/ 59 | **/Properties/launchSettings.json 60 | 61 | # StyleCop 62 | StyleCopReport.xml 63 | 64 | # Files built by Visual Studio 65 | *_i.c 66 | *_p.c 67 | *_i.h 68 | *_h.h 69 | *.ilk 70 | *.meta 71 | *.obj 72 | *.iobj 73 | *.pch 74 | *.pdb 75 | *.ipdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.tmp 84 | *.tmp_proj 85 | *_wpftmp.csproj 86 | *.log 87 | *.vspscc 88 | *.vssscc 89 | .builds 90 | *.pidb 91 | *.svclog 92 | *.scc 93 | 94 | # Chutzpah Test files 95 | _Chutzpah* 96 | 97 | # Visual C++ cache files 98 | ipch/ 99 | *.aps 100 | *.ncb 101 | *.opendb 102 | *.opensdf 103 | *.sdf 104 | *.cachefile 105 | *.VC.db 106 | *.VC.VC.opendb 107 | 108 | # Visual Studio profiler 109 | *.psess 110 | *.vsp 111 | *.vspx 112 | *.sap 113 | 114 | # Visual Studio Trace Files 115 | *.e2e 116 | 117 | # TFS 2012 Local Workspace 118 | $tf/ 119 | 120 | # Guidance Automation Toolkit 121 | *.gpState 122 | 123 | # ReSharper is a .NET coding add-in 124 | _ReSharper*/ 125 | *.[Rr]e[Ss]harper 126 | *.DotSettings.user 127 | 128 | # JustCode is a .NET coding add-in 129 | .JustCode 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # The packages folder can be ignored because of Package Restore 189 | **/[Pp]ackages/* 190 | # except build/, which is used as an MSBuild target. 191 | !**/[Pp]ackages/build/ 192 | # Uncomment if necessary however generally it will be regenerated when needed 193 | #!**/[Pp]ackages/repositories.config 194 | # NuGet v3's project.json files produces more ignorable files 195 | *.nuget.props 196 | *.nuget.targets 197 | 198 | # Microsoft Azure Build Output 199 | csx/ 200 | *.build.csdef 201 | 202 | # Microsoft Azure Emulator 203 | ecf/ 204 | rcf/ 205 | 206 | # Windows Store app package directories and files 207 | AppPackages/ 208 | BundleArtifacts/ 209 | Package.StoreAssociation.xml 210 | _pkginfo.txt 211 | *.appx 212 | 213 | # Visual Studio cache files 214 | # files ending in .cache can be ignored 215 | *.[Cc]ache 216 | # but keep track of directories ending in .cache 217 | !*.[Cc]ache/ 218 | 219 | # Others 220 | ClientBin/ 221 | ~$* 222 | *~ 223 | *.dbmdl 224 | *.dbproj.schemaview 225 | *.jfm 226 | *.pfx 227 | *.publishsettings 228 | orleans.codegen.cs 229 | 230 | # Including strong name files can present a security risk 231 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 232 | #*.snk 233 | 234 | # Since there are multiple workflows, uncomment next line to ignore bower_components 235 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 236 | #bower_components/ 237 | # ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true 238 | **/wwwroot/lib/ 239 | 240 | # RIA/Silverlight projects 241 | Generated_Code/ 242 | 243 | # Backup & report files from converting an old project file 244 | # to a newer Visual Studio version. Backup files are not needed, 245 | # because we have git ;-) 246 | _UpgradeReport_Files/ 247 | Backup*/ 248 | UpgradeLog*.XML 249 | UpgradeLog*.htm 250 | ServiceFabricBackup/ 251 | *.rptproj.bak 252 | 253 | # SQL Server files 254 | *.mdf 255 | *.ldf 256 | *.ndf 257 | 258 | # Business Intelligence projects 259 | *.rdl.data 260 | *.bim.layout 261 | *.bim_*.settings 262 | *.rptproj.rsuser 263 | 264 | # Microsoft Fakes 265 | FakesAssemblies/ 266 | 267 | # GhostDoc plugin setting file 268 | *.GhostDoc.xml 269 | 270 | # Node.js Tools for Visual Studio 271 | .ntvs_analysis.dat 272 | node_modules/ 273 | 274 | # Visual Studio 6 build log 275 | *.plg 276 | 277 | # Visual Studio 6 workspace options file 278 | *.opt 279 | 280 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 281 | *.vbw 282 | 283 | # Visual Studio LightSwitch build output 284 | **/*.HTMLClient/GeneratedArtifacts 285 | **/*.DesktopClient/GeneratedArtifacts 286 | **/*.DesktopClient/ModelManifest.xml 287 | **/*.Server/GeneratedArtifacts 288 | **/*.Server/ModelManifest.xml 289 | _Pvt_Extensions 290 | 291 | # Paket dependency manager 292 | .paket/paket.exe 293 | paket-files/ 294 | 295 | # FAKE - F# Make 296 | .fake/ 297 | 298 | # JetBrains Rider 299 | .idea/ 300 | *.sln.iml 301 | 302 | # CodeRush 303 | .cr/ 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # Byte-compiled / optimized / DLL files 346 | __pycache__/ 347 | *.py[cod] 348 | *$py.class 349 | 350 | # C extensions 351 | *.so 352 | 353 | # Distribution / packaging 354 | .Python 355 | build/ 356 | develop-eggs/ 357 | dist/ 358 | downloads/ 359 | eggs/ 360 | .eggs/ 361 | lib/ 362 | lib64/ 363 | parts/ 364 | sdist/ 365 | var/ 366 | wheels/ 367 | share/python-wheels/ 368 | *.egg-info/ 369 | .installed.cfg 370 | *.egg 371 | MANIFEST 372 | 373 | # PyInstaller 374 | # Usually these files are written by a python script from a template 375 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 376 | *.manifest 377 | *.spec 378 | 379 | # Installer logs 380 | pip-log.txt 381 | pip-delete-this-directory.txt 382 | 383 | # Unit test / coverage reports 384 | htmlcov/ 385 | .tox/ 386 | .nox/ 387 | .coverage 388 | .coverage.* 389 | .cache 390 | nosetests.xml 391 | coverage.xml 392 | *.cover 393 | .hypothesis/ 394 | .pytest_cache/ 395 | 396 | # Translations 397 | *.mo 398 | *.pot 399 | 400 | # Django stuff: 401 | *.log 402 | local_settings.py 403 | db.sqlite3 404 | 405 | # Flask stuff: 406 | instance/ 407 | .webassets-cache 408 | 409 | # Scrapy stuff: 410 | .scrapy 411 | 412 | # Sphinx documentation 413 | docs/_build/ 414 | 415 | # PyBuilder 416 | target/ 417 | 418 | # Jupyter Notebook 419 | .ipynb_checkpoints 420 | 421 | # IPython 422 | profile_default/ 423 | ipython_config.py 424 | 425 | # pyenv 426 | .python-version 427 | 428 | # celery beat schedule file 429 | celerybeat-schedule 430 | 431 | # SageMath parsed files 432 | *.sage.py 433 | 434 | # Environments 435 | .env 436 | .venv 437 | env/ 438 | venv/ 439 | ENV/ 440 | env.bak/ 441 | venv.bak/ 442 | 443 | # Spyder project settings 444 | .spyderproject 445 | .spyproject 446 | 447 | # Rope project settings 448 | .ropeproject 449 | 450 | # mkdocs documentation 451 | /site 452 | 453 | # mypy 454 | .mypy_cache/ 455 | .dmypy.json 456 | dmypy.json 457 | 458 | # Pyre type checker 459 | .pyre/ 460 | 461 | # DocFX 462 | docs/api 463 | _site/ 464 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "**/ci/*.yml": "azure-pipelines" 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATION NOTICE # 2 | 3 | **This repository is deprecated.** 4 | 5 | ## Microsoft.Jupyter.Core Preview ## 6 | 7 | The **Microsoft.Jupyter.Core** library makes it easier to write language kernels for Jupyter using .NET Core languages like C# and F#. 8 | This library uses .NET Core technologies such as the [ASP.NET Core Dependency Injection Framework](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2) to help make it straightforward to develop for the Jupyter platform. 9 | 10 | Kernels developed using **Microsoft.Jupyter.Core** can be installed as [.NET Core Global Tools](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools-how-to-create). 11 | This makes it easy to package, distribute, and install language kernels. 12 | For instance, the [**IEcho**](examples/echo-kernel/) sample kernel can be installed into a user's Jupyter environment with two commands: 13 | 14 | ``` 15 | cd examples/echo-kernel/ 16 | dotnet run -- install 17 | ``` 18 | 19 | Once installed, the IEcho example can then be used like any other Jupyter kernel by running your favorite client: 20 | 21 | ``` 22 | jupyter notebook 23 | ``` 24 | 25 | After a language kernel has been published as a NuGet package, it can be installed into a user's Jupyter environment with two commands. For example, if the IEcho kernel were published as a NuGet package named `Microsoft.Jupyter.Example.IEcho`, it could be installed via the following commands: 26 | 27 | ``` 28 | dotnet tool install -g Microsoft.Jupyter.Example.IEcho 29 | dotnet iecho install 30 | ```` 31 | 32 | ## Making New Language Kernels ## 33 | 34 | Using **Microsoft.Jupyter.Core** to make a new language kernel follows in several steps: 35 | 36 | - Create a new console application project in your favorite .NET Core language. 37 | - Add properties to your project to enable packaging as a [.NET Core Global Tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools-how-to-create). 38 | - Add `Microsoft.Jupyter.Core` as a package reference to your new project. 39 | - Add metadata properties for your new kernel. 40 | - Subclass the `BaseEngine` class. 41 | - Pass your metadata and engine to the `KernelApplication` class. 42 | 43 | Each of these steps has been demonstrated in the example kernels provided with the library: 44 | 45 | - [**IEcho**](examples/echo-kernel/): A simple language kernel that echos its input back as output. 46 | - [**IMoon**](examples/moon-kernel/): A language kernel for the [MoonSharp](http://moonsharp.org/) dialect of Lua. 47 | 48 | For more details, see the [provided tutorial](tutorial.md). 49 | 50 | ## Contributing ## 51 | 52 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 53 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 54 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 55 | 56 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 57 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 58 | provided by the bot. You will only need to do this once across all repos using our CLA. 59 | 60 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 61 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 62 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 63 | 64 | 65 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /ci/ci.yml: -------------------------------------------------------------------------------- 1 | name: $(Build.Major).$(Build.Minor).$(BuildId) 2 | trigger: 3 | - master 4 | 5 | schedules: 6 | - cron: "0 9 * * Sat" 7 | displayName: 'Build for Component Governance' 8 | branches: 9 | include: 10 | - master 11 | always: true 12 | 13 | resources: 14 | repositories: 15 | - repository: self 16 | variables: 17 | Build.Major: 3 18 | Build.Minor: 1 19 | Build.Patch: $(Build.BuildId) 20 | Build.Configuration: 'Release' 21 | VersioningScheme: 'byPrereleaseNumber' 22 | 23 | 24 | jobs: 25 | - job: Windows 26 | pool: 27 | vmImage: 'windows-2022' 28 | steps: 29 | - template: steps-build-core.yml 30 | - template: steps-build-samples.yml 31 | - task: PublishBuildArtifacts@1 32 | condition: succeededOrFailed() 33 | 34 | -------------------------------------------------------------------------------- /ci/signed.yml: -------------------------------------------------------------------------------- 1 | name: $(Build.Major).$(Build.Minor).$(BuildId) 2 | trigger: 3 | - master 4 | resources: 5 | repositories: 6 | - repository: self 7 | variables: 8 | Build.Major: 3 9 | Build.Minor: 1 10 | Build.Patch: $(Build.BuildId) 11 | Build.Configuration: 'Release' 12 | Assembly.Constants: 'SIGNED' 13 | VersioningScheme: 'byBuildNumber' 14 | DllsToScan: 'src\bin\Release\*\*.dll' 15 | 16 | jobs: 17 | - job: Windows 18 | pool: 19 | vmImage: 'windows-2022' 20 | steps: 21 | - template: steps-build-core.yml 22 | - template: steps-build-samples.yml 23 | - template: steps-sign.yml 24 | - template: steps-sdl-tools.yml 25 | - task: PublishBuildArtifacts@1 26 | condition: succeededOrFailed() 27 | -------------------------------------------------------------------------------- /ci/steps-build-core.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # Builds, tests & packs the Jupyter Core project 3 | ## 4 | 5 | steps: 6 | - task: DotNetCoreInstaller@1 7 | displayName: 'Install .NET Core SDK 3.1.300' 8 | inputs: 9 | version: 3.1.100 10 | 11 | - task: DotNetCoreCLI@2 12 | displayName: "Build and run unit tests on Core library" 13 | inputs: 14 | command: test 15 | arguments: '-c $(Build.Configuration) -v n /p:DefineConstants=$(Assembly.Constants)' 16 | projects: tests/core/core.csproj 17 | 18 | - task: DotNetCoreCLI@2 19 | displayName: "Pack Core library" 20 | inputs: 21 | command: pack 22 | configuration: $(Build.Configuration) 23 | packagesToPack: src/jupyter-core.csproj 24 | versioningScheme: $(VersioningScheme) 25 | majorVersion: $(Build.Major) 26 | minorVersion: $(Build.Minor) 27 | patchVersion: $(Build.Patch) -------------------------------------------------------------------------------- /ci/steps-build-samples.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # Runs sanity tests on the core and the sample kernels. 3 | ## 4 | 5 | steps: 6 | ## 7 | # The conda task is deprecated, so conda must be used inline. 8 | ## 9 | - pwsh: | 10 | $(CONDA)/scripts/conda.exe create --yes --quiet --name core_tests 11 | displayName: Create Anaconda environment 12 | 13 | - script: | 14 | $(CONDA)/scripts/conda.exe install --yes --quiet --name core_tests python=3.8 notebook jupyter_client pytest nose pip>=18.1 15 | displayName: Install Anaconda packages 16 | 17 | ## 18 | # Conda must be activated for every subsequent task. 19 | # The first line will execute shell functions that are dynamcally generated by conda and are a substitute for conda init. 20 | ## 21 | - pwsh: | 22 | (& "C:\Miniconda\scripts\conda.exe" "shell.powershell" "hook") | Out-String | Invoke-Expression 23 | conda activate core_tests 24 | conda info 25 | pip install jupyter_kernel_test 26 | displayName: 'Install additional dependencies from pip' 27 | 28 | # The version of pyzmq on Anaconda does not seem to work in Windows agents 29 | # as of November 2018: https://github.com/zeromq/pyzmq/issues/852 30 | - pwsh: | 31 | (& "C:\Miniconda\scripts\conda.exe" "shell.powershell" "hook") | Out-String | Invoke-Expression 32 | conda activate core_tests 33 | pip uninstall -y pyzmq 34 | pip install pyzmq 35 | displayName: 'Fix pyzmq on Windows' 36 | condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) 37 | 38 | - pwsh: | 39 | (& "C:\Miniconda\scripts\conda.exe" "shell.powershell" "hook") | Out-String | Invoke-Expression 40 | conda activate core_tests 41 | dotnet run -c $(Build.Configuration) -v n -- install --develop 42 | displayName: "Add IEcho kernel to Jupyter." 43 | workingDirectory: examples/echo-kernel 44 | 45 | - pwsh: | 46 | (& "C:\Miniconda\scripts\conda.exe" "shell.powershell" "hook") | Out-String | Invoke-Expression 47 | conda activate core_tests 48 | dotnet run -c $(Build.Configuration) -v n -- install --develop 49 | displayName: "Add IMoon kernel to Jupyter." 50 | workingDirectory: examples/moon-kernel 51 | 52 | - pwsh: | 53 | (& "C:\Miniconda\scripts\conda.exe" "shell.powershell" "hook") | Out-String | Invoke-Expression 54 | conda activate core_tests 55 | py.test tests/protocol/ 56 | displayName: "Run Jupyter protocol tests." 57 | workingDirectory: . 58 | 59 | -------------------------------------------------------------------------------- /ci/steps-sdl-tools.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # Runs scanning tools from security team. 3 | # See https://www.1eswiki.com/wiki/Secure_Development_Tools_Extension_For_Azure_DevOps 4 | ## 5 | steps: 6 | - task: ComponentGovernanceComponentDetection@0 7 | displayName: 'Components Detection' 8 | inputs: 9 | snapshotForceEnabled: true 10 | condition: and(succeededOrFailed(), variables.SDLScan) 11 | 12 | 13 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-antimalware.AntiMalware@3 14 | displayName: 'SDL: Anti-Malware scan of build sources and/or artifacts' 15 | continueOnError: true 16 | condition: and(succeededOrFailed(), variables.SDLScan) 17 | 18 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-autoapplicability.AutoApplicability@1 19 | displayName: 'SDL: Run AutoApplicability' 20 | inputs: 21 | ExternalRelease: true 22 | continueOnError: true 23 | condition: and(succeededOrFailed(), variables.SDLScan) 24 | 25 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-binskim.BinSkim@3 26 | displayName: 'SDL: Analyze managed and unmanaged binaries (exe, dll) for security vulnerabilities (BinSkim)' 27 | inputs: 28 | InputType: Basic 29 | AnalyzeTarget: $(DllsToScan) 30 | AnalyzeVerbose: true 31 | AnalyzeHashes: true 32 | continueOnError: true 33 | condition: and(succeededOrFailed(), variables.SDLScan) 34 | 35 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-codemetrics.CodeMetrics@1 36 | displayName: 'SDL: Analyze complexity of managed C# code (CodeMetrics)' 37 | inputs: 38 | Files: $(DllsToScan) 39 | continueOnError: true 40 | condition: and(succeededOrFailed(), variables.SDLScan) 41 | 42 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2 43 | displayName: 'SDL: Analyze source and build output text files for credentials (CredScan)' 44 | inputs: 45 | debugMode: false 46 | continueOnError: true 47 | condition: and(succeededOrFailed(), variables.SDLScan) 48 | 49 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-fxcop.FxCop@2 50 | displayName: 'SDL: Analyze C# code (.NET framework only) for security vulnerabilities (FxCop)' 51 | inputs: 52 | inputType: Basic 53 | targets: $(DllsToScan) 54 | continueOnError: true 55 | condition: and(succeededOrFailed(), variables.SDLScan) 56 | 57 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-prefast.SDLNativeRules@2 58 | displayName: 'SDL: Run the PREfast SDL Native Rules for MSBuild' 59 | continueOnError: true 60 | condition: and(succeededOrFailed(), variables.SDLScan) 61 | 62 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-vulnerabilityassessment.VulnerabilityAssessment@0 63 | displayName: 'SDL: Create Vulnerability Assessment' 64 | continueOnError: true 65 | condition: and(succeededOrFailed(), variables.SDLScan) 66 | 67 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-publishsecurityanalysislogs.PublishSecurityAnalysisLogs@2 68 | displayName: 'SDL: Publish Security Analysis Logs' 69 | continueOnError: true 70 | condition: and(succeededOrFailed(), variables.SDLScan) 71 | 72 | - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@1 73 | displayName: 'SDL: Post Analysis' 74 | inputs: 75 | BinSkim: true 76 | CredScan: true 77 | SDLNativeRules: true 78 | continueOnError: true 79 | condition: and(succeededOrFailed(), variables.SDLScan) 80 | -------------------------------------------------------------------------------- /ci/steps-sign.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # Signing dlls & packages. 3 | ## 4 | steps: 5 | 6 | - task: DotNetCoreInstaller@1 7 | displayName: 'Install .NET Core SDK 2.2.401' 8 | inputs: 9 | version: 2.2.401 10 | 11 | - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 12 | displayName: 'Signing: strong name' 13 | inputs: 14 | ConnectedServiceName: CodeSign 15 | FolderPath: src\bin\$(Build.Configuration) 16 | CertificateId: 267 17 | OpusName: 'Microsoft Jupyter Core' 18 | OpusInfo: 'https://github.com/Microsoft/jupyter-core' 19 | SessionTimeout: 120 20 | 21 | 22 | - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 23 | displayName: 'Signing: authenticode' 24 | inputs: 25 | ConnectedServiceName: CodeSign 26 | FolderPath: src\bin\$(Build.Configuration) 27 | CertificateId: 400 28 | OpusName: 'Microsoft Jupyter Core' 29 | OpusInfo: 'https://github.com/Microsoft/jupyter-core' 30 | SessionTimeout: 120 31 | 32 | 33 | - task: DotNetCoreCLI@2 34 | displayName: "Pack signed version of Core library" 35 | inputs: 36 | command: pack 37 | configuration: $(Build.Configuration) 38 | nobuild: true 39 | packagesToPack: src/jupyter-core.csproj 40 | versioningScheme: $(VersioningScheme) 41 | majorVersion: $(Build.Major) 42 | minorVersion: $(Build.Minor) 43 | patchVersion: $(Build.Patch) 44 | 45 | 46 | - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 47 | displayName: 'Signing: Nugets' 48 | inputs: 49 | ConnectedServiceName: CodeSign 50 | FolderPath: '$(Build.ArtifactStagingDirectory)' 51 | Pattern: '*.nupkg' 52 | signConfigType: inlineSignParams 53 | inlineOperation: | 54 | [ 55 | { 56 | "keyCode": "CP-401405", 57 | "operationSetCode": "NuGetSign", 58 | "parameters": [ ], 59 | "toolName": "sign", 60 | "toolVersion": "1.0" 61 | }, 62 | { 63 | "keyCode": "CP-401405", 64 | "operationSetCode": "NuGetVerify", 65 | "parameters": [ ], 66 | "toolName": "sign", 67 | "toolVersion": "1.0" 68 | } 69 | ] 70 | SessionTimeout: 120 71 | 72 | 73 | -------------------------------------------------------------------------------- /docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "files": [ 7 | "src/**.csproj" 8 | ] 9 | } 10 | ], 11 | "dest": "docs/api", 12 | "disableGitFeatures": false, 13 | "disableDefaultFilter": false 14 | } 15 | ], 16 | "build": { 17 | "content": [ 18 | { 19 | "files": [ 20 | "docs/api/**.yml", 21 | "docs/api/index.md" 22 | ] 23 | }, 24 | { 25 | "files": [ 26 | "docs/articles/**.md", 27 | "docs/articles/**/toc.yml", 28 | "docs/toc.yml", 29 | "docs/*.md" 30 | ] 31 | } 32 | ], 33 | "resource": [ 34 | { 35 | "files": [ 36 | "docs/images/**" 37 | ] 38 | } 39 | ], 40 | "overwrite": [ 41 | { 42 | "files": [ 43 | "apidoc/**.md" 44 | ], 45 | "exclude": [ 46 | "obj/**", 47 | "_site/**" 48 | ] 49 | } 50 | ], 51 | "dest": "_site", 52 | "globalMetadataFiles": [], 53 | "fileMetadataFiles": [], 54 | "template": [ 55 | "default" 56 | ], 57 | "postProcessors": [], 58 | "markdownEngineName": "markdig", 59 | "noLangKeyword": false, 60 | "keepFileLink": false, 61 | "cleanupCacheHistory": false, 62 | "disableGitFeatures": false 63 | } 64 | } -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # folder # 3 | ############### 4 | /**/DROP/ 5 | /**/TEMP/ 6 | /**/packages/ 7 | /**/bin/ 8 | /**/obj/ 9 | _site 10 | -------------------------------------------------------------------------------- /docs/articles/intro.md: -------------------------------------------------------------------------------- 1 | # Add your introductions here! 2 | -------------------------------------------------------------------------------- /docs/articles/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Introduction 2 | href: intro.md 3 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # This is the **HOMEPAGE**. 2 | Refer to [Markdown](http://daringfireball.net/projects/markdown/) for how to write markdown files. 3 | ## Quick Start Notes: 4 | 1. Add images to the *images* folder if the file is referencing an image. 5 | -------------------------------------------------------------------------------- /docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Articles 2 | href: articles/ 3 | - name: Api Documentation 4 | href: api/ 5 | -------------------------------------------------------------------------------- /examples/echo-kernel/Echo Kernel.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "text/plain": [ 11 | "%dne" 12 | ] 13 | }, 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "output_type": "execute_result" 17 | } 18 | ], 19 | "source": [ 20 | "%dne" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "outputs": [ 28 | { 29 | "data": { 30 | "text/plain": [ 31 | "bar" 32 | ] 33 | }, 34 | "execution_count": 2, 35 | "metadata": {}, 36 | "output_type": "execute_result" 37 | } 38 | ], 39 | "source": [ 40 | "bar" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 3, 46 | "metadata": {}, 47 | "outputs": [ 48 | { 49 | "data": { 50 | "text/plain": [ 51 | "baz" 52 | ] 53 | }, 54 | "execution_count": 3, 55 | "metadata": {}, 56 | "output_type": "execute_result" 57 | } 58 | ], 59 | "source": [ 60 | "baz" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 4, 66 | "metadata": {}, 67 | "outputs": [ 68 | { 69 | "data": { 70 | "text/html": [ 71 | "" 72 | ], 73 | "text/plain": [ 74 | "%dne, bar, baz, %history" 75 | ] 76 | }, 77 | "execution_count": 4, 78 | "metadata": {}, 79 | "output_type": "execute_result" 80 | } 81 | ], 82 | "source": [ 83 | "%history" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 5, 89 | "metadata": {}, 90 | "outputs": [ 91 | { 92 | "data": { 93 | "text/html": [ 94 | "
ComponentVersion
iecho1.0.0.0
Jupyter Core1.0.0.0
.NET Runtime.NETCoreApp,Version=v3.0
" 95 | ], 96 | "text/plain": [ 97 | "Component Version\r\n", 98 | "------------ ------------------------\r\n", 99 | "iecho 1.0.0.0\r\n", 100 | "Jupyter Core 1.0.0.0\r\n", 101 | ".NET Runtime .NETCoreApp,Version=v3.0\r\n" 102 | ] 103 | }, 104 | "metadata": {}, 105 | "output_type": "display_data" 106 | } 107 | ], 108 | "source": [ 109 | "%version" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 6, 115 | "metadata": {}, 116 | "outputs": [ 117 | { 118 | "data": { 119 | "text/html": [ 120 | "

%history

Displays a list of commands run so far this session.

" 121 | ], 122 | "text/plain": [ 123 | "%history:\n", 124 | "Displays a list of commands run so far this session." 125 | ] 126 | }, 127 | "execution_count": 6, 128 | "metadata": {}, 129 | "output_type": "execute_result" 130 | } 131 | ], 132 | "source": [ 133 | "%history?" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [] 142 | } 143 | ], 144 | "metadata": { 145 | "kernelspec": { 146 | "display_name": "IEcho", 147 | "language": "Echo", 148 | "name": "iecho" 149 | }, 150 | "language_info": { 151 | "file_extension": ".txt", 152 | "mimetype": "text/plain", 153 | "name": "Echo", 154 | "version": "0.1" 155 | } 156 | }, 157 | "nbformat": 4, 158 | "nbformat_minor": 2 159 | } 160 | -------------------------------------------------------------------------------- /examples/echo-kernel/EchoEngine.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Collections.Generic; 7 | using Microsoft.Extensions.Options; 8 | using Microsoft.Extensions.Logging; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Microsoft.Jupyter.Core 13 | { 14 | 15 | public class EchoEngine : BaseEngine 16 | { 17 | public EchoEngine( 18 | IShellServer shell, 19 | IShellRouter router, 20 | IOptions context, 21 | ILogger logger, 22 | IServiceProvider serviceProvider 23 | ) : base(shell, router, context, logger, serviceProvider) { } 24 | 25 | public override async Task ExecuteMundane(string input, IChannel channel) => 26 | (Program.ShoutOption.HasValue() ? input.ToUpper() : input).ToExecutionResult(); 27 | 28 | public override async Task> CompleteMundane(string code, int cursorPos) 29 | { 30 | Logger.LogInformation("Got completion request inside the echo kernel!"); 31 | return new List 32 | { 33 | new Completion 34 | { 35 | CursorStart = cursorPos, 36 | CursorEnd = cursorPos, 37 | Text = "foo" 38 | }, 39 | new Completion 40 | { 41 | CursorStart = cursorPos, 42 | CursorEnd = cursorPos, 43 | Text = "bar" 44 | } 45 | }; 46 | } 47 | 48 | [MagicCommand( 49 | "%tick", 50 | "Writes some ticks to demonstrate updatable display data." 51 | )] 52 | public async Task ExecuteTick(string code, IChannel channel) 53 | { 54 | var tickMessage = "Tick."; 55 | var updatable = channel.DisplayUpdatable(tickMessage); 56 | 57 | foreach (var idx in Enumerable.Range(0, 10)) 58 | { 59 | tickMessage += "."; 60 | Thread.Sleep(1000); 61 | updatable.Update(tickMessage); 62 | } 63 | 64 | return ExecuteStatus.Ok.ToExecutionResult(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/echo-kernel/KernelProperties.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Jupyter.Core 7 | { 8 | internal static class Constants 9 | { 10 | internal static KernelProperties PROPERTIES = new KernelProperties 11 | { 12 | // Name of the kernel as it appears outside of code. 13 | FriendlyName = "IEcho", 14 | // A short name for the kernel to be used in code, such as when 15 | // calling from jupyter_client. 16 | KernelName = "iecho", 17 | // The version of the kernel. 18 | KernelVersion = typeof(Program).Assembly.GetName().Version.ToString(), 19 | // Name of the kernel as it should appear in lists of available kernels. 20 | DisplayName = "Echo", 21 | 22 | // Name of the language implemented by the kernel. 23 | // Note that this property is used to set the syntax highlighting 24 | // mode in some clients, such as Jupyter Notebook. 25 | LanguageName = "Echo", 26 | // Version of the language implemeted by the kernel. 27 | LanguageVersion = "0.1", 28 | // The MIME type of the language implemented by the kernel. 29 | // This property is used mainly for providing "plain" downloads from 30 | // Jupyter clients. 31 | LanguageMimeType = MimeTypes.PlainText, 32 | // The file extension for source files written in the language 33 | // implemented by the kernel. 34 | // This property is used mainly for providing "plain" downloads from 35 | // Jupyter clients. 36 | LanguageFileExtension = ".txt", 37 | 38 | // An extended description of the kernel. 39 | Description = "A simple kernel that echos its input." 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /examples/echo-kernel/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Diagnostics; 7 | using System.ComponentModel.DataAnnotations; 8 | using Microsoft.Extensions.Logging; 9 | using Newtonsoft.Json; 10 | using System.Collections.Generic; 11 | using static Microsoft.Jupyter.Core.Constants; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using McMaster.Extensions.CommandLineUtils; 14 | 15 | namespace Microsoft.Jupyter.Core 16 | { 17 | class Program 18 | { 19 | public static void Init(ServiceCollection serviceCollection) => 20 | serviceCollection 21 | // Start a new service for the ReplEngine. 22 | .AddSingleton(); 23 | 24 | internal static CommandOption ShoutOption; 25 | 26 | public static int Main(string[] args) { 27 | var app = new KernelApplication( 28 | PROPERTIES, 29 | Init 30 | ); 31 | CommandOption shoutInstallOption = null; 32 | 33 | return app 34 | .AddInstallCommand( 35 | installCmd => { 36 | shoutInstallOption = installCmd.Option( 37 | "--shout", 38 | "Shout back when echoing input.", 39 | CommandOptionType.NoValue 40 | ); 41 | } 42 | ) 43 | .AddKernelCommand( 44 | kernelCmd => { 45 | ShoutOption = kernelCmd.Option( 46 | "--shout", 47 | "Shout back when echoing input.", 48 | CommandOptionType.NoValue 49 | ); 50 | } 51 | ) 52 | .WithKernelArguments( 53 | () => shoutInstallOption.HasValue() ? new[] {"--shout"} : new string[] {} 54 | ) 55 | .WithKernelSpecResources( 56 | new Dictionary 57 | { 58 | ["logo-64x64.png"] = "echo-kernel.res.logo-64x64.png" 59 | } 60 | ) 61 | .Execute(args); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/echo-kernel/README.md: -------------------------------------------------------------------------------- 1 | # IEcho Example Kernel # 2 | 3 | This project provides an example of how to write your own Jupyter kernels using Microsoft.Jupyter.Core. 4 | The kernel implemented by this project is a simple "echo" kernel that repeats its input as output. 5 | 6 | ## Installing from NuGet.org ## 7 | 8 | ``` 9 | dotnet tool install -g Microsoft.Jupyter.Example.IEcho 10 | dotnet iecho install 11 | ``` 12 | 13 | ## Installing from a Local Build ## 14 | 15 | ``` 16 | cd echo-kernel 17 | dotnet pack 18 | dotnet tool install -g Microsoft.Jupyter.Example.IEcho --add-source .\bin\Debug\ 19 | dotnet iecho install 20 | ``` 21 | 22 | ## Installing for Local Development ## 23 | 24 | ``` 25 | cd echo-kernel 26 | dotnet run -- install --develop 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/echo-kernel/echo-kernel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | Exe 6 | netcoreapp3.1 7 | 8 | 12 | dotnet-iecho 13 | Microsoft.Jupyter.Example.IEcho 14 | 1.0 15 | 16 | true 17 | DotnetCliTool 18 | Microsoft 19 | An example Jupyter kernel that echoes its input back as output. 20 | © Microsoft Corporation. All rights reserved. 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/echo-kernel/res/logo-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/jupyter-core/15a08a57215b0683b59bb04298c58a4f2bbb6aa0/examples/echo-kernel/res/logo-64x64.png -------------------------------------------------------------------------------- /examples/moon-kernel/DynValueConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MoonSharp.Interpreter; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Microsoft.Jupyter.Core 8 | { 9 | public class DynValueConverter : JsonConverter 10 | { 11 | public override DynValue ReadJson(JsonReader reader, Type objectType, DynValue existingValue, bool hasExistingValue, JsonSerializer serializer) 12 | { 13 | throw new NotImplementedException(); 14 | } 15 | 16 | public override void WriteJson(JsonWriter writer, DynValue value, JsonSerializer serializer) 17 | { 18 | var obj = value.ToObject(); 19 | JToken token; 20 | switch (obj) 21 | { 22 | case MoonSharp.Interpreter.Closure closure: 23 | token = JToken.FromObject(new Dictionary 24 | { 25 | ["@closure"] = closure.ToString() 26 | }); 27 | token.WriteTo(writer); 28 | return; 29 | 30 | default: 31 | // See https://github.com/JamesNK/Newtonsoft.Json/issues/386#issuecomment-421161191 32 | // for why this works to pass through. 33 | token = JToken.FromObject(obj); 34 | token.WriteTo(writer); 35 | return; 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /examples/moon-kernel/KernelProperties.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using MoonSharp.Interpreter.REPL; 5 | 6 | namespace Microsoft.Jupyter.Core 7 | { 8 | 9 | internal static class Constants 10 | { 11 | internal static KernelProperties PROPERTIES = new KernelProperties 12 | { 13 | FriendlyName = "IMoon", 14 | KernelName = "imoon", 15 | KernelVersion = typeof(Program).Assembly.GetName().Version.ToString(), 16 | DisplayName = "Lua (MoonScript)", 17 | 18 | LanguageName = "Lua", 19 | LanguageVersion = "0.1", 20 | LanguageMimeType = "text/plain", 21 | LanguageFileExtension = ".lua", 22 | 23 | Description = "Runs Lua using the MoonScript interpreter." 24 | } 25 | .WithAdditionalVersion("MoonScript"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/moon-kernel/MoonEngine.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Collections.Generic; 7 | using MoonSharp.Interpreter; 8 | using MoonSharp.Interpreter.REPL; 9 | using Microsoft.Extensions.Options; 10 | using Microsoft.Extensions.Logging; 11 | using Newtonsoft.Json; 12 | using System.Threading.Tasks; 13 | 14 | namespace Microsoft.Jupyter.Core 15 | { 16 | 17 | public class MoonEngine : BaseEngine 18 | { 19 | private ReplInterpreter interp; 20 | private Action printFn = null; 21 | 22 | public MoonEngine( 23 | IShellServer shell, 24 | IShellRouter router, 25 | IOptions context, 26 | ILogger logger, 27 | IServiceProvider serviceProvider 28 | ) : base(shell, router, context, logger, serviceProvider) 29 | { 30 | RegisterJsonEncoder( 31 | new DynValueConverter() 32 | ); 33 | RegisterDisplayEncoder( 34 | MimeTypes.Markdown, 35 | displayable => { 36 | if (displayable is IEnumerable list) 37 | { 38 | return String.Join( 39 | "\n", 40 | list.Select(item => $"- {item}") 41 | ); 42 | } 43 | else return $"`{displayable}`"; 44 | } 45 | ); 46 | var script = new Script(); 47 | script.Options.DebugPrint = str => printFn?.Invoke(str); 48 | interp = new ReplInterpreter(script); 49 | } 50 | 51 | public override async Task ExecuteMundane(string input, IChannel channel) 52 | { 53 | var oldAction = printFn; 54 | printFn = channel.Stdout; 55 | try 56 | { 57 | var result = interp.Evaluate(input); 58 | if (result == null) 59 | { 60 | channel.Stderr("Interpreter returned null, this is typically due to incomplete input."); 61 | return ExecuteStatus.Error.ToExecutionResult(); 62 | } 63 | else if (result.ToObject() != null) 64 | { 65 | return result.ToExecutionResult(); 66 | } 67 | else 68 | { 69 | return ExecuteStatus.Ok.ToExecutionResult(); 70 | } 71 | } 72 | catch (Exception ex) 73 | { 74 | channel.Stderr(ex.ToString()); 75 | return ExecuteStatus.Error.ToExecutionResult(); 76 | } 77 | finally 78 | { 79 | printFn = oldAction; 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/moon-kernel/MoonScript Kernel.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "application/json": "4.0", 11 | "text/markdown": [ 12 | "`4`" 13 | ], 14 | "text/plain": [ 15 | "4" 16 | ] 17 | }, 18 | "execution_count": 1, 19 | "metadata": {}, 20 | "output_type": "execute_result" 21 | } 22 | ], 23 | "source": [ 24 | "return 1 + 3" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "metadata": {}, 31 | "outputs": [ 32 | { 33 | "data": { 34 | "application/json": "120.0", 35 | "text/markdown": [ 36 | "`120`" 37 | ], 38 | "text/plain": [ 39 | "120" 40 | ] 41 | }, 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "output_type": "execute_result" 45 | } 46 | ], 47 | "source": [ 48 | "-- defines a factorial function\n", 49 | " function fact (n)\n", 50 | " if (n == 0) then\n", 51 | " return 1\n", 52 | " else\n", 53 | " return n*fact(n - 1)\n", 54 | " end\n", 55 | " end\n", 56 | "\n", 57 | " return fact(5)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 3, 63 | "metadata": {}, 64 | "outputs": [ 65 | { 66 | "data": { 67 | "application/json": "{\"@closure\":\"MoonSharp.Interpreter.Closure\"}", 68 | "text/markdown": [ 69 | "`(Function 00000074)`" 70 | ], 71 | "text/plain": [ 72 | "(Function 00000074)" 73 | ] 74 | }, 75 | "execution_count": 3, 76 | "metadata": {}, 77 | "output_type": "execute_result" 78 | } 79 | ], 80 | "source": [ 81 | "return fact" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 4, 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "data": { 91 | "application/json": "[\"return 1 + 3\",\"-- defines a factorial function\\n function fact (n)\\n if (n == 0) then\\n return 1\\n else\\n return n*fact(n - 1)\\n end\\n end\\n\\n return fact(5)\",\"return fact\",\"%history\"]", 92 | "text/html": [ 93 | "
  • return 1 + 3
  • -- defines a factorial function\n", 94 | " function fact (n)\n", 95 | " if (n == 0) then\n", 96 | " return 1\n", 97 | " else\n", 98 | " return n*fact(n - 1)\n", 99 | " end\n", 100 | " end\n", 101 | "\n", 102 | " return fact(5)
  • return fact
  • %history
" 103 | ], 104 | "text/markdown": [ 105 | "- return 1 + 3\n", 106 | "- -- defines a factorial function\n", 107 | " function fact (n)\n", 108 | " if (n == 0) then\n", 109 | " return 1\n", 110 | " else\n", 111 | " return n*fact(n - 1)\n", 112 | " end\n", 113 | " end\n", 114 | "\n", 115 | " return fact(5)\n", 116 | "- return fact\n", 117 | "- %history" 118 | ], 119 | "text/plain": [ 120 | "return 1 + 3, -- defines a factorial function\n", 121 | " function fact (n)\n", 122 | " if (n == 0) then\n", 123 | " return 1\n", 124 | " else\n", 125 | " return n*fact(n - 1)\n", 126 | " end\n", 127 | " end\n", 128 | "\n", 129 | " return fact(5), return fact, %history" 130 | ] 131 | }, 132 | "execution_count": 4, 133 | "metadata": {}, 134 | "output_type": "execute_result" 135 | } 136 | ], 137 | "source": [ 138 | "%history" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": 5, 144 | "metadata": {}, 145 | "outputs": [ 146 | { 147 | "data": { 148 | "application/json": "720.0", 149 | "text/markdown": [ 150 | "`720`" 151 | ], 152 | "text/plain": [ 153 | "720" 154 | ] 155 | }, 156 | "execution_count": 5, 157 | "metadata": {}, 158 | "output_type": "execute_result" 159 | } 160 | ], 161 | "source": [ 162 | "return fact(6)" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 6, 168 | "metadata": {}, 169 | "outputs": [ 170 | { 171 | "name": "stdout", 172 | "output_type": "stream", 173 | "text": [ 174 | "Hello, world" 175 | ] 176 | } 177 | ], 178 | "source": [ 179 | "print('Hello, world')" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 7, 185 | "metadata": {}, 186 | "outputs": [ 187 | { 188 | "data": { 189 | "application/json": "{\"Name\":\"%history\",\"Kind\":0,\"Documentation\":{\"Full\":null,\"Summary\":\"Displays a list of commands run so far this session.\"}}", 190 | "text/html": [ 191 | "

%history

Displays a list of commands run so far this session.

" 192 | ], 193 | "text/markdown": [ 194 | "`Microsoft.Jupyter.Core.MagicSymbol`" 195 | ], 196 | "text/plain": [ 197 | "%history:\n", 198 | "Displays a list of commands run so far this session." 199 | ] 200 | }, 201 | "execution_count": 7, 202 | "metadata": {}, 203 | "output_type": "execute_result" 204 | } 205 | ], 206 | "source": [ 207 | "%history?" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 8, 213 | "metadata": {}, 214 | "outputs": [ 215 | { 216 | "data": { 217 | "application/json": "{\"rows\":[{\"Item1\":\"imoon\",\"Item2\":\"1.0.0.0\"},{\"Item1\":\"Jupyter Core\",\"Item2\":\"1.0.0.0\"},{\"Item1\":\".NET Runtime\",\"Item2\":\".NETCoreApp,Version=v3.0\"},{\"Item1\":\"MoonScript\",\"Item2\":\"2.0.0.0\"}]}", 218 | "text/html": [ 219 | "
ComponentVersion
imoon1.0.0.0
Jupyter Core1.0.0.0
.NET Runtime.NETCoreApp,Version=v3.0
MoonScript2.0.0.0
" 220 | ], 221 | "text/markdown": [ 222 | "`Microsoft.Jupyter.Core.Table`1[System.ValueTuple`2[System.String,System.String]]`" 223 | ], 224 | "text/plain": [ 225 | "Component Version\r\n", 226 | "------------ ------------------------\r\n", 227 | "imoon 1.0.0.0\r\n", 228 | "Jupyter Core 1.0.0.0\r\n", 229 | ".NET Runtime .NETCoreApp,Version=v3.0\r\n", 230 | "MoonScript 2.0.0.0\r\n" 231 | ] 232 | }, 233 | "metadata": {}, 234 | "output_type": "display_data" 235 | } 236 | ], 237 | "source": [ 238 | "%version" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": null, 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [] 247 | } 248 | ], 249 | "metadata": { 250 | "kernelspec": { 251 | "display_name": "IMoon", 252 | "language": "Lua", 253 | "name": "imoon" 254 | }, 255 | "language_info": { 256 | "file_extension": ".lua", 257 | "mimetype": "text/plain", 258 | "name": "Lua", 259 | "version": "0.1" 260 | } 261 | }, 262 | "nbformat": 4, 263 | "nbformat_minor": 2 264 | } 265 | -------------------------------------------------------------------------------- /examples/moon-kernel/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Diagnostics; 7 | using System.ComponentModel.DataAnnotations; 8 | using Microsoft.Extensions.Logging; 9 | using Newtonsoft.Json; 10 | using System.Collections.Generic; 11 | using static Microsoft.Jupyter.Core.Constants; 12 | using Microsoft.Extensions.DependencyInjection; 13 | 14 | namespace Microsoft.Jupyter.Core 15 | { 16 | class Program 17 | { 18 | public static void Init(ServiceCollection serviceCollection) => 19 | serviceCollection 20 | // Start a new service for the ReplEngine. 21 | .AddSingleton(); 22 | 23 | public static int Main(string[] args) { 24 | var app = new KernelApplication( 25 | PROPERTIES, 26 | Init 27 | ) 28 | .ConfigureLogging(loggingBuilder => 29 | { 30 | loggingBuilder.AddFile( 31 | Path.Join(Directory.GetCurrentDirectory(), "imoon.log"), 32 | minimumLevel: LogLevel.Debug 33 | ); 34 | }); 35 | 36 | return app.WithDefaultCommands().Execute(args); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/moon-kernel/moon-kernel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dotnet-imoon 5 | True 6 | Exe 7 | netcoreapp3.1 8 | 1.0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /jupyter-core.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "jupyter-core", "src\jupyter-core.csproj", "{D1C7D204-B6C5-4789-8035-ED8B594DCEB3}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{EF78B627-9418-4608-A773-349165C55BFC}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "moon-kernel", "examples\moon-kernel\moon-kernel.csproj", "{C5D59812-BFC0-4AFB-8D81-9D7F0C362079}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "echo-kernel", "examples\echo-kernel\echo-kernel.csproj", "{95C93365-CD6B-46E7-8CFC-2F10D4E42536}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{99727866-8D93-42FC-9E35-7A7ED15EFB46}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "core", "tests\core\core.csproj", "{551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Debug|x64.ActiveCfg = Debug|Any CPU 34 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Debug|x64.Build.0 = Debug|Any CPU 35 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Debug|x86.ActiveCfg = Debug|Any CPU 36 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Debug|x86.Build.0 = Debug|Any CPU 37 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Release|x64.ActiveCfg = Release|Any CPU 40 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Release|x64.Build.0 = Release|Any CPU 41 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Release|x86.ActiveCfg = Release|Any CPU 42 | {D1C7D204-B6C5-4789-8035-ED8B594DCEB3}.Release|x86.Build.0 = Release|Any CPU 43 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Debug|x64.ActiveCfg = Debug|Any CPU 46 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Debug|x64.Build.0 = Debug|Any CPU 47 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Debug|x86.ActiveCfg = Debug|Any CPU 48 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Debug|x86.Build.0 = Debug|Any CPU 49 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Release|x64.ActiveCfg = Release|Any CPU 52 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Release|x64.Build.0 = Release|Any CPU 53 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Release|x86.ActiveCfg = Release|Any CPU 54 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079}.Release|x86.Build.0 = Release|Any CPU 55 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Debug|x64.ActiveCfg = Debug|Any CPU 58 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Debug|x64.Build.0 = Debug|Any CPU 59 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Debug|x86.ActiveCfg = Debug|Any CPU 60 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Debug|x86.Build.0 = Debug|Any CPU 61 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Release|x64.ActiveCfg = Release|Any CPU 64 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Release|x64.Build.0 = Release|Any CPU 65 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Release|x86.ActiveCfg = Release|Any CPU 66 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536}.Release|x86.Build.0 = Release|Any CPU 67 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Debug|x64.ActiveCfg = Debug|Any CPU 70 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Debug|x64.Build.0 = Debug|Any CPU 71 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Debug|x86.ActiveCfg = Debug|Any CPU 72 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Debug|x86.Build.0 = Debug|Any CPU 73 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Release|x64.ActiveCfg = Release|Any CPU 76 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Release|x64.Build.0 = Release|Any CPU 77 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Release|x86.ActiveCfg = Release|Any CPU 78 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214}.Release|x86.Build.0 = Release|Any CPU 79 | EndGlobalSection 80 | GlobalSection(NestedProjects) = preSolution 81 | {C5D59812-BFC0-4AFB-8D81-9D7F0C362079} = {EF78B627-9418-4608-A773-349165C55BFC} 82 | {95C93365-CD6B-46E7-8CFC-2F10D4E42536} = {EF78B627-9418-4608-A773-349165C55BFC} 83 | {551FFA2A-BE5F-4DB0-92C5-654F5EBAD214} = {99727866-8D93-42FC-9E35-7A7ED15EFB46} 84 | EndGlobalSection 85 | EndGlobal 86 | -------------------------------------------------------------------------------- /src/Data/ConnectionInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Net; 5 | using System.Text; 6 | using System.Linq; 7 | using NetMQ; 8 | using Newtonsoft.Json; 9 | using System.Collections.Generic; 10 | using System.Security.Cryptography; 11 | using System; 12 | using Microsoft.Jupyter.Core.Protocol; 13 | 14 | namespace Microsoft.Jupyter.Core 15 | { 16 | /// 17 | /// Represents the information stored in a Jupyter connection file. 18 | /// See https://jupyter-client.readthedocs.io/en/stable/kernels.html#connection-files 19 | /// for details. 20 | /// 21 | [JsonObject(MemberSerialization.OptIn)] 22 | public class ConnectionInfo 23 | { 24 | // As per documentation at https://jupyter-client.readthedocs.io/en/stable/kernels.html#connection-files, 25 | // an example connection file looks like the following: 26 | // 27 | // { 28 | // "control_port": 50160, 29 | // "shell_port": 57503, 30 | // "transport": "tcp", 31 | // "signature_scheme": "hmac-sha256", 32 | // "stdin_port": 52597, 33 | // "hb_port": 42540, 34 | // "ip": "127.0.0.1", 35 | // "iopub_port": 40885, 36 | // "key": "a0436f6c-1916-498b-8eb9-e81ab9368e84" 37 | // } 38 | 39 | #region Port Information 40 | 41 | [JsonProperty("control_port")] 42 | public int ControlPort { get; set; } 43 | 44 | [JsonProperty("shell_port")] 45 | public int ShellPort { get; set; } 46 | 47 | [JsonProperty("hb_port")] 48 | public int HeartbeatPort { get; set; } 49 | 50 | [JsonProperty("iopub_port")] 51 | public int IoPubPort { get; set; } 52 | 53 | [JsonProperty("stdin_port")] 54 | public int StdInPort { get; set; } 55 | 56 | #endregion 57 | 58 | #region Transport Information 59 | 60 | [JsonProperty("transport")] 61 | public Transport Transport { get; set; } 62 | 63 | [JsonProperty("ip")] 64 | [JsonConverter(typeof(IpAddressConverter))] 65 | public IPAddress IpAddress { get; set; } 66 | 67 | #endregion 68 | 69 | #region Authentication Information 70 | 71 | [JsonProperty("key")] 72 | public string Key { get; set; } 73 | 74 | [JsonProperty("signature_scheme")] 75 | public SignatureScheme SignatureScheme { get; set; } 76 | 77 | #endregion 78 | 79 | #region Metadata 80 | 81 | [JsonProperty("kernel_name")] 82 | public string KernelName { get; set; } 83 | 84 | #endregion 85 | 86 | #region ZeroMQ Configuration 87 | // ZeroMQ uses a notion of "address" that combines both IP addresses 88 | // and ports, similar to a URL. Thus, we put some logic here to 89 | // construct ZeroMQ addresses from the rest of the connection info. 90 | 91 | // FIXME: consolidate address logic here. 92 | public string HeartbeatZmqAddress { 93 | get { 94 | var protocol = Enum.GetName(typeof(Transport), Transport).ToLower(); 95 | return $"{protocol}://{IpAddress}:{HeartbeatPort}"; 96 | } 97 | } 98 | 99 | public string ShellZmqAddress { 100 | get { 101 | var protocol = Enum.GetName(typeof(Transport), Transport).ToLower(); 102 | return $"{protocol}://{IpAddress}:{ShellPort}"; 103 | } 104 | } 105 | 106 | public string ControlZmqAddress { 107 | get { 108 | var protocol = Enum.GetName(typeof(Transport), Transport).ToLower(); 109 | return $"{protocol}://{IpAddress}:{ControlPort}"; 110 | } 111 | } 112 | 113 | public string IoPubZmqAddress { 114 | get { 115 | var protocol = Enum.GetName(typeof(Transport), Transport).ToLower(); 116 | return $"{protocol}://{IpAddress}:{IoPubPort}"; 117 | } 118 | } 119 | #endregion 120 | 121 | #region Diagnostic Support 122 | 123 | public override string ToString() => 124 | JsonConvert.SerializeObject(this, Formatting.Indented); 125 | 126 | #endregion 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Data/Enumerations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Converters; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.Immutable; 9 | using System.Linq; 10 | using System.Net; 11 | using System.Runtime.Serialization; 12 | using System.Security.Cryptography; 13 | using System.Text; 14 | 15 | namespace Microsoft.Jupyter.Core 16 | { 17 | 18 | [JsonConverter(typeof(StringEnumConverter))] 19 | public enum ExecuteStatus 20 | { 21 | [EnumMember(Value="ok")] 22 | Ok, 23 | 24 | [EnumMember(Value="error")] 25 | Error, 26 | 27 | [EnumMember(Value="abort")] 28 | Abort 29 | } 30 | 31 | [JsonConverter(typeof(StringEnumConverter))] 32 | public enum CompleteStatus 33 | { 34 | [EnumMember(Value="ok")] 35 | Ok, 36 | 37 | [EnumMember(Value="error")] 38 | Error 39 | } 40 | 41 | [JsonConverter(typeof(StringEnumConverter))] 42 | public enum ExecutionState 43 | { 44 | [EnumMember(Value="busy")] 45 | Busy, 46 | 47 | [EnumMember(Value="idle")] 48 | Idle, 49 | 50 | [EnumMember(Value="starting")] 51 | Starting 52 | } 53 | 54 | [JsonConverter(typeof(StringEnumConverter))] 55 | public enum StreamName 56 | { 57 | [EnumMember(Value="stdin")] 58 | StandardIn, 59 | 60 | [EnumMember(Value="stdout")] 61 | StandardOut, 62 | 63 | [EnumMember(Value="stderr")] 64 | StandardError 65 | } 66 | 67 | public enum Transport 68 | { 69 | [EnumMember(Value="tcp")] 70 | Tcp 71 | } 72 | 73 | public enum SignatureScheme 74 | { 75 | [EnumMember(Value="hmac-sha256")] 76 | HmacSha256 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Data/KernelContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using Microsoft.Extensions.Logging; 9 | using Newtonsoft.Json; 10 | 11 | namespace Microsoft.Jupyter.Core 12 | { 13 | /// 14 | /// Describes all information needed to identify a kernel at runtime, 15 | /// including how to connect to clients, and a description of the 16 | /// kernel's supported language. 17 | /// 18 | public class KernelContext 19 | { 20 | public ConnectionInfo ConnectionInfo { get; private set; } 21 | public KernelProperties Properties { get; set; } 22 | 23 | /// 24 | /// Populates the context using a connection file, typically 25 | /// provided by Jupyter when instantiating a kernel. 26 | /// 27 | /// 28 | /// A path to the connection file to be loaded. 29 | /// 30 | /// 31 | /// A logger object used to report debugging information from the 32 | /// connection file. 33 | /// 34 | public void LoadConnectionFile(string connectionFile, ILogger logger = null) 35 | { 36 | logger?.LogDebug("Loading kernel context from connection file: {connectionFile}.", connectionFile); 37 | this.ConnectionInfo = JsonConvert.DeserializeObject(File.ReadAllText(connectionFile)); 38 | logger?.LogDebug("Loaded connection information:\n{connectionInfo}", this.ConnectionInfo); 39 | } 40 | 41 | internal HMAC NewHmac() { 42 | // TODO: ensure that this HMAC algorithm agrees with that specified 43 | // in ConnectionInfo. 44 | return new HMACSHA256(Encoding.ASCII.GetBytes(ConnectionInfo.Key)); 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/Data/Metadata.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net; 8 | using System.Runtime.Serialization; 9 | using Microsoft.Jupyter.Core.Protocol; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Runtime.Versioning; 14 | 15 | namespace Microsoft.Jupyter.Core 16 | { 17 | 18 | [JsonObject(MemberSerialization.OptIn)] 19 | public class HelpLinks 20 | { 21 | [JsonProperty("text")] 22 | public string Text { get; set; } 23 | 24 | [JsonProperty("url")] 25 | public Uri Url { get; set; } 26 | } 27 | 28 | [JsonObject(MemberSerialization.OptIn)] 29 | public class KernelSpec 30 | { 31 | [JsonProperty("argv")] 32 | public List Arguments; 33 | 34 | [JsonProperty("display_name")] 35 | public string DisplayName; 36 | 37 | [JsonProperty("language")] 38 | public string LanguageName; 39 | 40 | [JsonProperty("interrupt_mode")] 41 | public string InterruptMode = "message"; 42 | } 43 | 44 | /// 45 | /// Specifies the metadata for a particular language kernel, including 46 | /// details about the kernel, the language supported by the kernel, 47 | /// and relevant version information. 48 | /// 49 | /// 50 | /// 51 | /// var properties = new KernelProperties 52 | /// { 53 | /// FriendlyName = "IEcho", 54 | /// KernelName = "iecho", 55 | /// KernelVersion = typeof(Program).Assembly.GetName().Version.ToString(), 56 | /// DisplayName = "Echo", 57 | /// 58 | /// LanguageName = "Echo", 59 | /// LanguageVersion = "0.1", 60 | /// LanguageMimeType = MimeTypes.PlainText, 61 | /// LanguageFileExtension = ".txt", 62 | /// 63 | /// Description = "A simple kernel that echos its input." 64 | /// }; 65 | /// 66 | /// 67 | public class KernelProperties 68 | { 69 | private IList<(string, string)> additionalVersions = new List<(string, string)>(); 70 | 71 | /// 72 | /// A user-friendly name for the kernel, typically used in menus. 73 | /// 74 | public string FriendlyName { get; set; } 75 | 76 | /// 77 | /// The name for the kernel, this name will be used to register the kernel with Jupyter 78 | /// and to identify the kernel programmatically. 79 | /// 80 | public string KernelName { get; set; } 81 | 82 | /// 83 | /// A string describing the version of the kernel. 84 | /// 85 | /// 86 | /// Note that this property does not refer to the version of the 87 | /// language supported by the kernel. 88 | /// 89 | public string KernelVersion { get; set; } 90 | 91 | public string DisplayName { get; set; } 92 | 93 | 94 | /// 95 | /// The name of the language supported by the kernel. 96 | /// 97 | public string LanguageName { get; set; } 98 | /// 99 | /// A string describing the version of the language supported by 100 | /// the kernel. 101 | /// 102 | public string LanguageVersion { get; set; } 103 | 104 | /// 105 | /// The MIME type used for source files in the supported language. 106 | /// 107 | /// 108 | /// This property is typically used by clients to offer exports of 109 | /// notebooks as plain script or source files. 110 | /// 111 | public string LanguageMimeType { get; set; } 112 | 113 | /// 114 | /// The file extension used for source files in the supported 115 | /// language. 116 | /// 117 | /// 118 | /// This property is typically used by clients to offer exports of 119 | /// notebooks as plain script or source files. 120 | /// 121 | public string LanguageFileExtension { get; set; } 122 | 123 | /// 124 | /// An extended description of the kernel. 125 | /// 126 | public string Description { get; set; } 127 | 128 | /// 129 | /// A collection of links to more information on this kernel and 130 | /// its supported language. 131 | /// 132 | public HelpLinks[] HelpLinks { get; set; } 133 | 134 | /// 135 | /// Returns a list of versions for the various components used by 136 | /// this kernel. 137 | /// 138 | /// 139 | /// Note that versions are represented as strings rather than 140 | /// System.Version instances in order to represent version 141 | /// numbers that may not conform to .NET versioning standards. 142 | /// 143 | public virtual IEnumerable<(string, string)> VersionTable { 144 | get { 145 | yield return (KernelName, KernelVersion); 146 | yield return ("Jupyter Core", typeof(KernelProperties).Assembly.GetName().Version.ToString()); 147 | yield return ( 148 | ".NET Runtime", 149 | // Use the technique documented at 150 | // https://weblog.west-wind.com/posts/2018/Apr/12/Getting-the-NET-Core-Runtime-Version-in-a-Running-Application 151 | // to get the target framework moniker of the entry point for the kernel, 152 | // so that we can report that as a version. 153 | Assembly 154 | .GetEntryAssembly() 155 | ?.GetCustomAttribute() 156 | ?.FrameworkName ?? "" 157 | ); 158 | foreach (var version in additionalVersions) yield return version; 159 | } 160 | } 161 | 162 | public LanguageInfo AsLanguageInfo() 163 | { 164 | return new LanguageInfo 165 | { 166 | Name = LanguageName, 167 | LanguageVersion = LanguageVersion, 168 | MimeType = LanguageMimeType, 169 | FileExtension = LanguageFileExtension 170 | }; 171 | } 172 | 173 | internal KernelInfoReplyContent AsKernelInfoReply() 174 | { 175 | return new KernelInfoReplyContent 176 | { 177 | Implementation = KernelName, 178 | ImplementationVersion = KernelVersion, 179 | LanguageInfo = AsLanguageInfo(), 180 | HelpLinks = new HelpLinks[0], 181 | ExecuteStatus = ExecuteStatus.Ok, 182 | Banner = "" 183 | }; 184 | } 185 | 186 | /// 187 | /// Registers an additional component in the version table reported 188 | /// by this kernel. 189 | /// 190 | /// 191 | /// The name of the component as should be reported to the user. 192 | /// 193 | /// 194 | /// The version of the component as should be reported to the user. 195 | /// 196 | public KernelProperties WithAdditionalVersion(string component, string version) 197 | { 198 | this.additionalVersions.Add((component, version)); 199 | return this; 200 | } 201 | 202 | /// 203 | /// Registers an additional component in the version table reported 204 | /// by this kernel, using the version information provided by the 205 | /// assembly for a given type. 206 | /// 207 | /// 208 | /// A type from the assembly whose version should be registered as 209 | /// the version of this component. 210 | /// 211 | /// 212 | /// The name of the component as should be reported to the user. 213 | /// 214 | public KernelProperties WithAdditionalVersion(string component) 215 | { 216 | this.additionalVersions.Add((component, typeof(T).Assembly.GetName().Version.ToString())); 217 | return this; 218 | } 219 | } 220 | 221 | [JsonObject(MemberSerialization.OptIn)] 222 | public class LanguageInfo 223 | { 224 | [JsonProperty("name")] 225 | public string Name { get; set; } 226 | 227 | [JsonProperty("version")] 228 | public string LanguageVersion {get; set;} 229 | 230 | [JsonProperty("mimetype")] 231 | public string MimeType {get; set;} 232 | 233 | [JsonProperty("file_extension")] 234 | public string FileExtension {get; set;} 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /src/Data/Serialization.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Net; 7 | using System.Runtime.Serialization; 8 | 9 | namespace Microsoft.Jupyter.Core 10 | { 11 | 12 | public class IpAddressConverter : JsonConverter 13 | { 14 | public override IPAddress ReadJson(JsonReader reader, Type objectType, IPAddress existingValue, bool hasExistingValue, JsonSerializer serializer) 15 | { 16 | if (reader.TokenType == JsonToken.Null) 17 | { 18 | return null; 19 | } 20 | else if (reader.TokenType == JsonToken.String) 21 | { 22 | return IPAddress.Parse((string) serializer.Deserialize(reader, typeof(string))); 23 | } 24 | else 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | } 29 | 30 | public override void WriteJson(JsonWriter writer, IPAddress value, JsonSerializer serializer) => 31 | writer.WriteValue(value.ToString()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Engines/CompleteRequestHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.Immutable; 9 | using System.Linq; 10 | using System.Reflection; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Newtonsoft.Json; 14 | using Microsoft.Jupyter.Core.Protocol; 15 | using System.Diagnostics; 16 | using System.Threading.Tasks; 17 | using System.Threading; 18 | using Microsoft.Extensions.DependencyInjection; 19 | 20 | namespace Microsoft.Jupyter.Core 21 | { 22 | internal class CompleteRequestHandler : IShellHandler 23 | { 24 | private BaseEngine engine; 25 | private IShellServer shellServer; 26 | private ILogger logger; 27 | 28 | public CompleteRequestHandler(IExecutionEngine engine, IShellServer shellServer, ILogger logger) 29 | { 30 | if (engine is BaseEngine baseEngine) 31 | { 32 | this.engine = baseEngine; 33 | } 34 | else throw new Exception("The CompleteRequestHandler requires that the IExecutionEngine service inherits from BaseEngine."); 35 | this.shellServer = shellServer; 36 | this.logger = logger; 37 | } 38 | 39 | public string MessageType => "complete_request"; 40 | 41 | public async Task HandleAsync(Message message) 42 | { 43 | await engine.Initialized; 44 | 45 | var request = message.Content as CompleteRequestContent; 46 | if (request == null) 47 | { 48 | logger.LogError("Expected completion result content, but got {Type} instead.", message.Content.GetType()); 49 | return; 50 | } 51 | this.logger.LogDebug("Ask to complete code at cursor position {CursorPos}:\n{Code}", request.CursorPos, request.Code); 52 | 53 | shellServer.NotifyBusyStatus(message, ExecutionState.Busy); 54 | try 55 | { 56 | var completion = await engine.Complete(request.Code, request.CursorPos); 57 | // If we got a completion back from the engine that was anything 58 | // other than null, respond with it here. Note that unlike execute_request, 59 | // it's ok to just ignore a complete request that we don't know 60 | // how to handle. 61 | if (completion != null) 62 | { 63 | this.shellServer.SendShellMessage( 64 | new Message 65 | { 66 | Content = new CompleteReplyContent 67 | { 68 | CompleteStatus = completion.Value.Status, 69 | Matches = completion.Value.Matches.ToList(), 70 | CursorStart = completion.Value.CursorStart ?? request.CursorPos, 71 | CursorEnd = completion.Value.CursorEnd ?? request.CursorPos 72 | }, 73 | Header = new MessageHeader 74 | { 75 | MessageType = "complete_reply" 76 | } 77 | } 78 | .AsReplyTo(message) 79 | ); 80 | } 81 | return; 82 | } 83 | catch (TaskCanceledException tce) 84 | { 85 | this.logger?.LogDebug(tce, "Task cancelled."); 86 | return; 87 | } 88 | catch (Exception e) 89 | { 90 | this.logger?.LogError(e, "Unable to process CompleteRequest."); 91 | return; 92 | } 93 | finally 94 | { 95 | shellServer.NotifyBusyStatus(message, ExecutionState.Idle); 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/Engines/CompletionResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | using Newtonsoft.Json.Linq; 12 | 13 | namespace Microsoft.Jupyter.Core 14 | { 15 | 16 | // From the Jupyter messaging protocol documentation 17 | // at https://jupyter-client.readthedocs.io/en/stable/messaging.html#completion: 18 | // 19 | // content = { 20 | // # status should be 'ok' unless an exception was raised during the request, 21 | // # in which case it should be 'error', along with the usual error message content 22 | // # in other messages. 23 | // 'status' : 'ok' 24 | // 25 | // # The list of all matches to the completion request, such as 26 | // # ['a.isalnum', 'a.isalpha'] for the above example. 27 | // 'matches' : list, 28 | // 29 | // # The range of text that should be replaced by the above matches when a completion is accepted. 30 | // # typically cursor_end is the same as cursor_pos in the request. 31 | // 'cursor_start' : int, 32 | // 'cursor_end' : int, 33 | // 34 | // # Information that frontend plugins might use for extra display information about completions. 35 | // 'metadata' : dict, 36 | // } 37 | 38 | /// 39 | /// Represents a list of completion results returned by an execution 40 | /// engine. 41 | /// 42 | public struct CompletionResult 43 | { 44 | public CompleteStatus Status; 45 | public IList? Matches; 46 | public int? CursorStart; 47 | public int? CursorEnd; 48 | public IDictionary Metadata; 49 | 50 | public static CompletionResult Failed => new CompletionResult 51 | { 52 | Status = CompleteStatus.Ok, 53 | Matches = null, 54 | CursorStart = null, 55 | CursorEnd = null, 56 | Metadata = new Dictionary() 57 | }; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Engines/ExecuteRequestHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.Immutable; 9 | using System.Linq; 10 | using System.Reflection; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Newtonsoft.Json; 14 | using Microsoft.Jupyter.Core.Protocol; 15 | using System.Diagnostics; 16 | using System.Threading.Tasks; 17 | using System.Threading; 18 | using Microsoft.Extensions.DependencyInjection; 19 | 20 | namespace Microsoft.Jupyter.Core 21 | { 22 | internal class ExecuteRequestHandler : OrderedShellHandler 23 | { 24 | 25 | private BaseEngine engine; 26 | private IShellServer shellServer; 27 | 28 | private Task? previousTask = null; 29 | private ILogger? logger = null; 30 | private ICommsRouter router; 31 | 32 | protected override ILogger? Logger => logger; 33 | 34 | /// 35 | /// The number of cells that have been executed since the start of 36 | /// this engine. Used by clients to typeset cell numbers, e.g.: 37 | /// In[12]:. 38 | /// 39 | public int ExecutionCount { get; protected set; } = 0; 40 | 41 | public ExecuteRequestHandler(IExecutionEngine engine, IShellServer shellServer, ICommsRouter router, ILogger logger) 42 | { 43 | if (engine is BaseEngine baseEngine) 44 | { 45 | this.engine = baseEngine; 46 | } 47 | else throw new Exception("The ExecuteRequestHandler requires that the IExecutionEngine service inherits from BaseEngine."); 48 | this.shellServer = shellServer; 49 | this.logger = logger; 50 | this.router = router; 51 | } 52 | 53 | public override string MessageType => "execute_request"; 54 | 55 | protected async virtual Task ExecutionTaskForMessage(Message message, int executionCount, Action onHandled) 56 | { 57 | var engineResponse = ExecutionResult.Aborted; 58 | 59 | var code = (message.Content as ExecuteRequestContent)?.Code ?? string.Empty; 60 | 61 | this.shellServer.SendIoPubMessage( 62 | new Message 63 | { 64 | ZmqIdentities = message.ZmqIdentities, 65 | ParentHeader = message.Header, 66 | Metadata = new Dictionary(), 67 | Content = new ExecuteInputContent 68 | { 69 | Code = code, 70 | ExecutionCount = executionCount 71 | }, 72 | Header = new MessageHeader 73 | { 74 | MessageType = "execute_input" 75 | } 76 | } 77 | ); 78 | 79 | using (var cancellationTokenSource = new CancellationTokenSource()) 80 | { 81 | Action onInterruptRequest = (message) => cancellationTokenSource.Cancel(); 82 | 83 | var shellServerSupportsInterrupt = this.shellServer as IShellServerSupportsInterrupt; 84 | if (shellServerSupportsInterrupt != null) 85 | { 86 | shellServerSupportsInterrupt.InterruptRequest += onInterruptRequest; 87 | } 88 | 89 | // Make sure that the engine is fully initialized. 90 | await engine.Initialized; 91 | 92 | try 93 | { 94 | engineResponse = await engine.Execute( 95 | code, 96 | new BaseEngine.ExecutionChannel(engine, message, router), 97 | cancellationTokenSource.Token 98 | ); 99 | } 100 | finally 101 | { 102 | if (shellServerSupportsInterrupt != null) 103 | { 104 | shellServerSupportsInterrupt.InterruptRequest -= onInterruptRequest; 105 | } 106 | } 107 | } 108 | 109 | // Send the engine's output as an execution result. 110 | if (engineResponse.Output != null) 111 | { 112 | var serialized = engine.EncodeForDisplay(engineResponse.Output); 113 | this.shellServer.SendIoPubMessage( 114 | new Message 115 | { 116 | ZmqIdentities = message.ZmqIdentities, 117 | ParentHeader = message.Header, 118 | Metadata = new Dictionary(), 119 | Content = new ExecuteResultContent 120 | { 121 | ExecutionCount = executionCount, 122 | Data = serialized.Data, 123 | Metadata = serialized.Metadata 124 | }, 125 | Header = new MessageHeader 126 | { 127 | MessageType = "execute_result" 128 | } 129 | } 130 | ); 131 | } 132 | 133 | // Invoke the onHandled callback prior to sending the execute_reply message. 134 | // This guarantees that the OrderedShellHandler correctly processes the completion 135 | // of this task *before* any processing new task that the client may submit for 136 | // execution immediately upon receiving the execute_reply message. 137 | onHandled(); 138 | 139 | // Handle the message. 140 | this.shellServer.SendShellMessage( 141 | new Message 142 | { 143 | ZmqIdentities = message.ZmqIdentities, 144 | ParentHeader = message.Header, 145 | Metadata = new Dictionary(), 146 | Content = new ExecuteReplyContent 147 | { 148 | ExecuteStatus = engineResponse.Status, 149 | ExecutionCount = executionCount, 150 | UserExpressions = new Dictionary() 151 | }, 152 | Header = new MessageHeader 153 | { 154 | MessageType = "execute_reply" 155 | } 156 | } 157 | ); 158 | 159 | return engineResponse; 160 | } 161 | 162 | protected async Task SendAbortMessage(Message message) 163 | { 164 | // The previous call failed, so abort here and let the 165 | // shell server know. 166 | this.shellServer.SendShellMessage( 167 | new Message 168 | { 169 | ZmqIdentities = message.ZmqIdentities, 170 | ParentHeader = message.Header, 171 | Metadata = new Dictionary(), 172 | Content = new ExecuteReplyContent 173 | { 174 | ExecuteStatus = ExecuteStatus.Abort, 175 | ExecutionCount = null, 176 | UserExpressions = new Dictionary() 177 | }, 178 | Header = new MessageHeader 179 | { 180 | MessageType = "execute_reply" 181 | } 182 | } 183 | ); 184 | 185 | // Finish by telling the client that we're free again. 186 | this.shellServer.SendIoPubMessage( 187 | new Message 188 | { 189 | Header = new MessageHeader 190 | { 191 | MessageType = "status" 192 | }, 193 | Content = new KernelStatusContent 194 | { 195 | ExecutionState = ExecutionState.Idle 196 | } 197 | }.AsReplyTo(message) 198 | ); 199 | } 200 | 201 | private int IncrementExecutionCount() 202 | { 203 | lock (this) 204 | { 205 | return ++this.ExecutionCount; 206 | } 207 | } 208 | 209 | public override async Task HandleAsync(Message message, ExecutionResult? previousResult) => 210 | await HandleAsync(message, previousResult, () => {}); 211 | 212 | public override async Task HandleAsync(Message message, ExecutionResult? previousResult, Action onHandled) 213 | { 214 | this.logger.LogDebug($"Asked to execute code:\n{((ExecuteRequestContent)message.Content).Code}"); 215 | 216 | if (previousResult != null && previousResult.Value.Status != ExecuteStatus.Ok) 217 | { 218 | this.logger.LogDebug("Aborting due to previous execution result indicating failure: {PreviousResult}", previousResult.Value); 219 | onHandled(); 220 | await SendAbortMessage(message); 221 | return ExecutionResult.Aborted; 222 | } 223 | 224 | var executionCount = IncrementExecutionCount(); 225 | shellServer.NotifyBusyStatus(message, ExecutionState.Busy); 226 | 227 | try 228 | { 229 | var result = await ExecutionTaskForMessage(message, executionCount, onHandled); 230 | return result; 231 | } 232 | catch (TaskCanceledException tce) 233 | { 234 | this.logger?.LogDebug(tce, "Task cancelled."); 235 | return new ExecutionResult 236 | { 237 | Output = null, 238 | Status = ExecuteStatus.Abort 239 | }; 240 | } 241 | catch (Exception e) 242 | { 243 | this.logger?.LogError(e, "Unable to process ExecuteRequest"); 244 | return new ExecutionResult 245 | { 246 | Output = e, 247 | Status = ExecuteStatus.Error 248 | }; 249 | } 250 | finally 251 | { 252 | shellServer.NotifyBusyStatus(message, ExecutionState.Idle); 253 | } 254 | } 255 | 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/Engines/ExecutionResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | using Newtonsoft.Json.Linq; 12 | 13 | namespace Microsoft.Jupyter.Core 14 | { 15 | public struct ExecutionResult 16 | { 17 | public ExecuteStatus Status; 18 | public object Output; 19 | 20 | public static ExecutionResult Failed => new ExecutionResult 21 | { 22 | Status = ExecuteStatus.Error, 23 | Output = null 24 | }; 25 | 26 | public static ExecutionResult Aborted => new ExecutionResult 27 | { 28 | Status = ExecuteStatus.Abort, 29 | Output = null 30 | }; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Engines/IChannel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using Microsoft.Jupyter.Core.Protocol; 8 | 9 | namespace Microsoft.Jupyter.Core 10 | { 11 | /// 12 | /// Represents a display output that can be updated after it is first 13 | /// rendered (e.g.: for displaying progress of long-running tasks, 14 | /// or for providing interactivity with the user). 15 | /// 16 | public interface IUpdatableDisplay 17 | { 18 | /// 19 | /// Replaces any previous content rendered to this display with a 20 | /// new displayable object. 21 | /// 22 | /// 23 | /// The object to be displayed. Cannot be null. 24 | /// 25 | void Update(object displayable); 26 | } 27 | 28 | /// 29 | /// Provided as a backwards compatability shim for implementations of 30 | /// IChannel that do not support updatable display. 31 | /// 32 | internal class UpdatableDisplayFallback : IUpdatableDisplay 33 | { 34 | private readonly IChannel channel; 35 | 36 | public UpdatableDisplayFallback(IChannel channel) 37 | { 38 | this.channel = channel; 39 | } 40 | 41 | public void Update(object displayable) 42 | { 43 | channel.Display(displayable); 44 | } 45 | } 46 | 47 | /// 48 | /// Specifies a display channel between a Jupyter kernel and its clients 49 | /// that can be used for printing to streams, displaying rich data, and 50 | /// sending messages. 51 | /// 52 | public interface IChannel 53 | { 54 | /// 55 | /// Writes a message to this channel's standard output stream. 56 | /// 57 | /// The message to be written. Cannot be null. 58 | void Stdout(string message); 59 | 60 | /// 61 | /// Writes a message to this channel's standard error stream. 62 | /// 63 | /// The message to be written. Cannot be null. 64 | void Stderr(string message); 65 | 66 | /// 67 | /// Displays an object using this display channel. 68 | /// 69 | /// The object to be displayed. Cannot be null. 70 | void Display(object displayable); 71 | 72 | /// 73 | /// Displays an object using this display channel and allows for the 74 | /// object to be updated with future calls. 75 | /// 76 | /// The object to be displayed. Cannot be null. 77 | /// An object that can be used to update the display. 78 | public IUpdatableDisplay DisplayUpdatable(object displayable) 79 | { 80 | this.Display(displayable); 81 | return new UpdatableDisplayFallback(this); 82 | } 83 | 84 | /// 85 | /// Sends an iopub message to the client associated with this channel. 86 | /// 87 | /// The message to send. Cannot be null. 88 | void SendIoPubMessage(Message message) => throw new NotImplementedException(); 89 | 90 | /// 91 | /// Gets the comms router associated with this channel, if any, 92 | /// or null if no comms router is available. 93 | /// 94 | /// 95 | /// Engines should ensure that this value is not null. In 96 | /// a future version, this property will no longer be nullable. 97 | /// 98 | public ICommsRouter? CommsRouter => null; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Engines/IExecutionEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.Jupyter.Core 7 | { 8 | public interface IExecutionEngine 9 | { 10 | void Start(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Engines/InputParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace Microsoft.Jupyter.Core 11 | { 12 | internal class InputParser 13 | { 14 | internal enum CommandType 15 | { 16 | Mundane, 17 | Magic, 18 | Help, 19 | MagicHelp 20 | } 21 | 22 | public ISymbolResolver Resolver; 23 | 24 | public InputParser(ISymbolResolver resolver) 25 | { 26 | this.Resolver = resolver; 27 | } 28 | 29 | public CommandType GetNextCommand(string input, out ISymbol? symbol, out string commandInput, out string remainingInput) 30 | { 31 | if (input == null) { throw new ArgumentNullException("input"); } 32 | 33 | symbol = null; 34 | commandInput = input; 35 | remainingInput = string.Empty; 36 | 37 | var inputLines = input.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).ToList(); 38 | if (inputLines == null || inputLines.Count == 0) 39 | { 40 | return CommandType.Mundane; 41 | } 42 | 43 | // Find the first non-whitespace line and see if it starts with a magic symbol. 44 | bool isHelp = false; 45 | int firstLineIndex = inputLines.FindIndex(s => !string.IsNullOrWhiteSpace(s)); 46 | if (firstLineIndex < 0 || !StartsWithMagic(inputLines[firstLineIndex], out symbol, out isHelp)) 47 | { 48 | // No magic symbol found. 49 | return isHelp ? CommandType.Help : CommandType.Mundane; 50 | } 51 | 52 | // Look through the remaining lines until we find one that 53 | // starts with a magic symbol. 54 | string? _commandInput = null; 55 | for (int lineIndex = firstLineIndex + 1; lineIndex < inputLines.Count; lineIndex++) 56 | { 57 | if (StartsWithMagic(inputLines[lineIndex], out _, out _)) 58 | { 59 | _commandInput = string.Join(Environment.NewLine, inputLines.SkipLast(inputLines.Count - lineIndex)); 60 | remainingInput = string.Join(Environment.NewLine, inputLines.Skip(lineIndex)); 61 | break; 62 | } 63 | } 64 | 65 | // If we didn't find another magic symbol, use the full input 66 | // as the command input. 67 | commandInput = _commandInput ?? input; 68 | 69 | return isHelp ? CommandType.MagicHelp : CommandType.Magic; 70 | } 71 | 72 | private bool StartsWithMagic(string input, out ISymbol? symbol, out bool isHelp) 73 | { 74 | symbol = null; 75 | isHelp = false; 76 | 77 | var inputParts = input.Trim().Split(null, 2); 78 | var symbolName = inputParts[0].Trim(); 79 | if (symbolName.StartsWith("?")) 80 | { 81 | symbolName = symbolName.Substring(1, symbolName.Length - 1); 82 | isHelp = true; 83 | } 84 | else if (symbolName.EndsWith("?")) 85 | { 86 | symbolName = symbolName.Substring(0, symbolName.Length - 1); 87 | isHelp = true; 88 | } 89 | 90 | if (!string.IsNullOrEmpty(symbolName)) 91 | { 92 | symbol = this.Resolver.Resolve(symbolName); 93 | } 94 | 95 | return (symbol as MagicSymbol) != null; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/Extensions/ChannelWithNewLines.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Text; 9 | using Microsoft.Jupyter.Core.Protocol; 10 | 11 | namespace Microsoft.Jupyter.Core 12 | { 13 | /// 14 | /// Provides extension methods used throughout the Jupyter Core library. 15 | /// 16 | public static partial class Extensions 17 | { 18 | /// 19 | /// Creates a wrapper of an IChannel that adds new lines to every message 20 | /// sent to stdout and stderr. 21 | /// 22 | /// 23 | /// If original is already a ChannelWithNewLines, this method 24 | /// simply returns original unmodified. 25 | /// 26 | public static ChannelWithNewLines WithNewLines(this IChannel original) => 27 | (original is ChannelWithNewLines ch) ? ch : new ChannelWithNewLines(original); 28 | } 29 | 30 | /// 31 | /// This is a Jupyter Core IChannel that wraps an existing IChannel and 32 | /// adds NewLine symbols (Environment.NewLine) 33 | /// to every message that gets logged to Stdout and Stderror. 34 | /// 35 | public class ChannelWithNewLines : IChannel 36 | { 37 | /// 38 | /// The existing channel that this channel wraps with new lines. 39 | /// 40 | public IChannel BaseChannel { get; } 41 | 42 | /// 43 | /// Constructs a new channel, given a base channel to be wrapped 44 | /// with newlines. 45 | /// 46 | public ChannelWithNewLines(IChannel original) => BaseChannel = original; 47 | 48 | /// 49 | /// Formats a given message for display to stdout or stderr. 50 | /// 51 | /// The message to be formatted. 52 | /// 53 | /// , formatted with a trailing newline 54 | /// (Environment.NewLine). 55 | /// 56 | public static string Format(string msg) => $"{msg}{Environment.NewLine}"; 57 | 58 | /// 59 | /// Writes a given message to the base channel's standard output, 60 | /// but with a trailing newline appended. 61 | /// 62 | /// The message to be written. 63 | public void Stdout(string message) => BaseChannel?.Stdout(Format(message)); 64 | 65 | /// 66 | /// Writes a given message to the base channel's standard error, 67 | /// but with a trailing newline appended. 68 | /// 69 | /// The message to be written. 70 | public void Stderr(string message) => BaseChannel?.Stderr(Format(message)); 71 | 72 | /// 73 | /// Displays a given object using the base channel. 74 | /// 75 | /// The object to be displayed. 76 | /// 77 | /// Note that no newline is appended by this method, as the 78 | /// displayable object need not be a string. 79 | /// 80 | public void Display(object displayable) => BaseChannel?.Display(displayable); 81 | 82 | /// 83 | /// Displays a given object using the base channel, allowing for 84 | /// future updates. 85 | /// 86 | /// The object to be displayed. 87 | /// 88 | /// Note that no newline is appended by this method, as the 89 | /// displayable object need not be a string. 90 | /// 91 | /// 92 | /// An object that can be used to update the display in the future. 93 | /// 94 | public IUpdatableDisplay DisplayUpdatable(object displayable) => BaseChannel?.DisplayUpdatable(displayable); 95 | 96 | /// 97 | public void SendIoPubMessage(Message message) => BaseChannel?.SendIoPubMessage(message); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Extensions/Collections.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Net; 5 | using System.Text; 6 | using System.Linq; 7 | using NetMQ; 8 | using Newtonsoft.Json; 9 | using System.Collections.Generic; 10 | using System.Security.Cryptography; 11 | using System; 12 | 13 | namespace Microsoft.Jupyter.Core 14 | { 15 | public static partial class Extensions 16 | { 17 | 18 | // NB: This is a polyfill for the equivalent .NET Core 2.0 method, not available in .NET Standard 2.0. 19 | public static TValue GetValueOrDefault(this IDictionary dict, TKey key, TValue @default) 20 | { 21 | var success = dict.TryGetValue(key, out var value); 22 | return success ? value : @default; 23 | } 24 | 25 | public static bool IsEqual(this T[] actual, T[] expected) 26 | where T: IEquatable 27 | { 28 | return Enumerable 29 | .Zip(actual, expected, (actualElement, expectedElement) => actualElement.Equals(expectedElement)) 30 | .Aggregate((acc, nextBool) => (acc && nextBool)); 31 | } 32 | 33 | public static IEnumerable AsEnumerable(this Nullable nullable) 34 | where T : struct 35 | { 36 | if (nullable.HasValue) 37 | { 38 | yield return nullable.Value; 39 | } 40 | } 41 | public static Dictionary Update(this Dictionary dict, Dictionary other) 42 | { 43 | foreach (var item in other) 44 | { 45 | dict[item.Key] = item.Value; 46 | } 47 | 48 | return dict; 49 | } 50 | 51 | public static void Deconstruct(this KeyValuePair entry, out TKey key, out TValue value) 52 | { 53 | key = entry.Key; 54 | value = entry.Value; 55 | } 56 | 57 | public static IEnumerable EnumerateInReverse(this IList source) 58 | { 59 | foreach (var idx in Enumerable.Range(1, source.Count)) 60 | { 61 | yield return source[source.Count - idx]; 62 | } 63 | } 64 | 65 | } 66 | } -------------------------------------------------------------------------------- /src/Extensions/Conversions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Net; 5 | using System.Text; 6 | using System.Linq; 7 | using NetMQ; 8 | using Newtonsoft.Json; 9 | using System.Collections.Generic; 10 | using System.Security.Cryptography; 11 | using System; 12 | 13 | namespace Microsoft.Jupyter.Core 14 | { 15 | public static partial class Extensions 16 | { 17 | 18 | /// 19 | /// Converts a string containing hexadecimal digits to an array of 20 | /// bytes representing the same data. 21 | /// 22 | /// 23 | /// A string containing an even number of hexadecimal characters 24 | /// (0-f). 25 | /// 26 | /// An array of bytes representing the same data. 27 | public static byte[] HexToBytes(this string hex) 28 | { 29 | var bytes = new byte[hex.Length / 2]; 30 | foreach (var idxHexPair in Enumerable.Range(0, hex.Length / 2)) 31 | { 32 | bytes[idxHexPair] = Convert.ToByte(hex.Substring(2 * idxHexPair, 2), 16); 33 | } 34 | return bytes; 35 | } 36 | 37 | /// 38 | /// Encapsulates encoded data into an 39 | /// value without metadata. 40 | /// 41 | public static EncodedData ToEncodedData(this string data) => 42 | new EncodedData 43 | { 44 | Data = data, 45 | Metadata = new Dictionary() 46 | }; 47 | 48 | /// 49 | /// Encapsulates an execution status as a result object without 50 | /// output. This is useful when an input being executed has 51 | /// completed, but has not produced output; e.g. after a print 52 | /// statement or function. 53 | /// 54 | /// 55 | /// The status to be encapsulated as the result of an execution. 56 | /// 57 | public static ExecutionResult ToExecutionResult(this ExecuteStatus status) => 58 | new ExecutionResult 59 | { 60 | Status = status, 61 | Output = null 62 | }; 63 | 64 | /// 65 | /// Encapsulates a given output as the result of an execution. 66 | /// By default, this method denotes that an execution completed 67 | /// successfully. 68 | /// 69 | /// 70 | /// The output from an execution. 71 | /// 72 | /// 73 | /// The status to be encapsulated as the result of an execution. 74 | /// 75 | public static ExecutionResult ToExecutionResult(this object output, ExecuteStatus status = ExecuteStatus.Ok) => 76 | new ExecutionResult 77 | { 78 | Status = status, 79 | Output = output 80 | }; 81 | 82 | } 83 | } -------------------------------------------------------------------------------- /src/Extensions/Cryptography.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Net; 5 | using System.Text; 6 | using System.Linq; 7 | using NetMQ; 8 | using Newtonsoft.Json; 9 | using System.Collections.Generic; 10 | using System.Security.Cryptography; 11 | using System; 12 | 13 | namespace Microsoft.Jupyter.Core 14 | { 15 | public static partial class Extensions 16 | { 17 | public static byte[] ComputeHash(this HMAC hmac, params byte[][] blobs) 18 | { 19 | hmac.Initialize(); 20 | // TODO: generalize to allow encodings other than UTF-8. 21 | foreach (var blob in blobs.Take(blobs.Length - 1)) 22 | { 23 | hmac.TransformBlock(blob, 0, blob.Length, null, 0); 24 | } 25 | var lastBlob = blobs[blobs.Length - 1]; 26 | hmac.TransformFinalBlock(lastBlob, 0, lastBlob.Length); 27 | return hmac.Hash; 28 | } 29 | 30 | public static byte[] ComputeHash(this HMAC hmac, params object[] blobs) 31 | { 32 | return hmac.ComputeHash( 33 | blobs 34 | .Select(blob => Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob))) 35 | .ToArray() 36 | ); 37 | } 38 | 39 | } 40 | } -------------------------------------------------------------------------------- /src/Extensions/Extensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Net; 5 | using System.Text; 6 | using System.Linq; 7 | using NetMQ; 8 | using Newtonsoft.Json; 9 | using System.Collections.Generic; 10 | using System.Security.Cryptography; 11 | using System; 12 | using Newtonsoft.Json.Linq; 13 | using Microsoft.Jupyter.Core.Protocol; 14 | 15 | namespace Microsoft.Jupyter.Core 16 | { 17 | /// 18 | /// Provides extension methods used throughout the Jupyter Core library. 19 | /// 20 | public static partial class Extensions 21 | { 22 | /// 23 | /// Given some raw data, attempts to decode it into an object of 24 | /// a given type, returning false if the deserialization 25 | /// fails. 26 | /// 27 | public static bool TryAs(this JToken rawData, out T converted) 28 | where T: class 29 | { 30 | try 31 | { 32 | converted = rawData.ToObject(); 33 | return true; 34 | } 35 | catch 36 | { 37 | converted = null; 38 | return false; 39 | } 40 | } 41 | 42 | internal static void NotifyBusyStatus(this IShellServer shellServer, Message message, ExecutionState state) 43 | { 44 | // Begin by sending that we're busy. 45 | shellServer.SendIoPubMessage( 46 | new Message 47 | { 48 | Header = new MessageHeader 49 | { 50 | MessageType = "status" 51 | }, 52 | Content = new KernelStatusContent 53 | { 54 | ExecutionState = state 55 | }, 56 | Metadata = new Dictionary() 57 | }.AsReplyTo(message) 58 | ); 59 | } 60 | 61 | internal static CompletionResult AsCompletionResult(this IEnumerable completions, string code, int cursorPos) 62 | { 63 | // Since Jupyter's messaging protocol assumes a single cursor start and end for all completions, we need 64 | // to make a common cursor start from the minimum cursor start, and similarly for the cursor end. 65 | var minCursor = cursorPos; 66 | var maxCursor = cursorPos; 67 | if (completions.Any()) 68 | { 69 | minCursor = completions.Min(completion => completion.CursorStart); 70 | maxCursor = completions.Max(completion => completion.CursorEnd); 71 | } 72 | 73 | return new CompletionResult 74 | { 75 | Status = CompleteStatus.Ok, 76 | CursorStart = minCursor, 77 | CursorEnd = maxCursor, 78 | Matches = completions 79 | .Select(completion => 80 | { 81 | var prefix = code.Substring(minCursor, completion.CursorStart - minCursor); 82 | var suffix = code.Substring(maxCursor, completion.CursorEnd - maxCursor); 83 | return prefix + completion.Text + suffix; 84 | }) 85 | .ToList() 86 | }; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Extensions/Messages.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Net; 5 | using System.Text; 6 | using System.Linq; 7 | using NetMQ; 8 | using Newtonsoft.Json; 9 | using System.Collections.Generic; 10 | using System.Security.Cryptography; 11 | using System; 12 | using Microsoft.Jupyter.Core.Protocol; 13 | using Newtonsoft.Json.Linq; 14 | 15 | namespace Microsoft.Jupyter.Core 16 | { 17 | public static partial class Extensions 18 | { 19 | 20 | /// 21 | /// Receives an entire Jupyter wire protocol message from a given 22 | /// ZeroMQ socket and deserializes it to a Message object for 23 | /// further processing. 24 | /// 25 | public static Message ReceiveMessage( 26 | this NetMQSocket socket, 27 | KernelContext context, 28 | Encoding encoding = null 29 | ) 30 | { 31 | encoding = encoding ?? Encoding.UTF8; 32 | 33 | // Get all the relevant message frames. 34 | var rawFrames = socket 35 | .ReceiveMultipartBytes(); 36 | var frames = rawFrames 37 | .Select(frame => encoding.GetString(frame)) 38 | .ToList(); 39 | 40 | // We know that one of the frames should be the special delimiter 41 | // . If we don't find it, time to throw an exception. 42 | var idxDelimiter = frames.IndexOf(""); 43 | if (idxDelimiter < 0) 44 | { 45 | throw new ProtocolViolationException("Expected delimiter, but none was present."); 46 | } 47 | 48 | // At this point, we know that everything before idxDelimter is 49 | // a ZMQ identity, and that everything after follows the Jupyter 50 | // wire protocol. In particular, the next five blobs after 51 | // are as follows: 52 | // • An HMAC signature for the entire message. 53 | // • A serialized header for this message. 54 | // • A serialized header for the previous message in sequence. 55 | // • A serialized metadata dictionary. 56 | // • A serialized content dictionary. 57 | // Any remaining blobs are extra raw data buffers. 58 | 59 | // We start by computing the digest, since that is much, much easier 60 | // to do given the raw frames than trying to unambiguously 61 | // reserialize everything. 62 | // To compute the digest and verify the message, we start by pulling 63 | // out the claimed signature. This is by default a string of 64 | // hexadecimal characters, so we convert to a byte[] for comparing 65 | // with the HMAC output. 66 | var signature = frames[idxDelimiter + 1].HexToBytes(); 67 | // Next, we take the four frames after the delimeter, since 68 | // those are the subject of the digest. 69 | var toDigest = rawFrames.Skip(idxDelimiter + 2).Take(4).ToArray(); 70 | var digest = context.NewHmac().ComputeHash(toDigest); 71 | 72 | if (!signature.IsEqual(digest)) 73 | { 74 | var digestStr = Convert.ToBase64String(digest); 75 | var signatureStr = Convert.ToBase64String(signature); 76 | throw new ProtocolViolationException( 77 | $"HMAC {digestStr} did not agree with {signatureStr}."); 78 | } 79 | 80 | // If we made it this far, we can unpack the content of the message 81 | // into the right subclass of MessageContent. 82 | var header = JsonConvert.DeserializeObject(frames[idxDelimiter + 2]); 83 | var content = MessageContent.Deserializers.GetValueOrDefault( 84 | header.MessageType, 85 | data => 86 | new UnknownContent 87 | { 88 | RawData = JToken.Parse(data) 89 | } 90 | )(frames[idxDelimiter + 5]); 91 | 92 | var message = new Message 93 | { 94 | ZmqIdentities = rawFrames.Take(idxDelimiter).ToList(), 95 | Signature = signature, 96 | Header = header, 97 | ParentHeader = JsonConvert.DeserializeObject(frames[idxDelimiter + 3]), 98 | Metadata = JsonConvert.DeserializeObject>(frames[idxDelimiter + 4]), 99 | Content = content 100 | }; 101 | 102 | return message; 103 | } 104 | 105 | public static void SendMessage(this IOutgoingSocket socket, KernelContext context, Message message) 106 | { 107 | // FIXME: need to handle parents for messages which are handled 108 | // sequentially. 109 | 110 | // Conceptually, sending a message consists of three steps: 111 | // • Convert the message to four frames. 112 | // • Digest the four frames. 113 | // • Send the identities, the delimeter, the digest, and the 114 | // message frames. 115 | 116 | var zmqMessage = new NetMQMessage(); 117 | var frames = new object[] 118 | { 119 | message.Header, 120 | message.ParentHeader, 121 | message.Metadata, 122 | message.Content 123 | } 124 | .Select(frame => JsonConvert.SerializeObject(frame)) 125 | .Select(str => Encoding.UTF8.GetBytes(str)) 126 | .ToList(); 127 | var digest = context.NewHmac().ComputeHash(frames.ToArray()); 128 | 129 | message.ZmqIdentities?.ForEach(ident => zmqMessage.Append(ident)); 130 | zmqMessage.Append(""); 131 | zmqMessage.Append(BitConverter.ToString(digest).Replace("-", "").ToLowerInvariant()); 132 | frames.ForEach(ident => zmqMessage.Append(ident)); 133 | 134 | socket.SendMultipartMessage(zmqMessage); 135 | } 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /src/Extensions/ServiceCollection.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Net; 5 | using System.Text; 6 | using System.Linq; 7 | using NetMQ; 8 | using Newtonsoft.Json; 9 | using System.Collections.Generic; 10 | using System.Security.Cryptography; 11 | using System; 12 | using Microsoft.Extensions.DependencyInjection; 13 | 14 | namespace Microsoft.Jupyter.Core 15 | { 16 | public static partial class Extensions 17 | { 18 | /// 19 | /// Adds the core kernel servers (heartbeat and shell) to the service collection. 20 | /// 21 | public static IServiceCollection AddKernelServers(this IServiceCollection serviceCollection) 22 | { 23 | serviceCollection 24 | .AddSingleton() 25 | .AddSingleton() 26 | .AddSingleton() 27 | .AddSingleton(); 28 | 29 | return serviceCollection; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Properties/267DevDivSNKey2048.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/jupyter-core/15a08a57215b0683b59bb04298c58a4f2bbb6aa0/src/Properties/267DevDivSNKey2048.snk -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Microsoft.Jupyter.Core.Tests" + SigningConstants.PUBLIC_KEY)] 4 | -------------------------------------------------------------------------------- /src/Properties/DelaySign.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | // Attributes for delay-signing 4 | #if SIGNED 5 | [assembly:AssemblyKeyFile(SigningConstants.KEY_FILE)] 6 | [assembly:AssemblyDelaySign(true)] 7 | #endif 8 | 9 | internal static partial class SigningConstants 10 | { 11 | #if SIGNED 12 | public const string KEY_FILE = @"Properties\267DevDivSNKey2048.snk"; 13 | public const string PUBLIC_KEY = ", PublicKey=" + 14 | "002400000c800000140100000602000000240000525341310008000001000100613399aff18ef1" + 15 | "a2c2514a273a42d9042b72321f1757102df9ebada69923e2738406c21e5b801552ab8d200a65a2" + 16 | "35e001ac9adc25f2d811eb09496a4c6a59d4619589c69f5baf0c4179a47311d92555cd006acc8b" + 17 | "5959f2bd6e10e360c34537a1d266da8085856583c85d81da7f3ec01ed9564c58d93d713cd0172c" + 18 | "8e23a10f0239b80c96b07736f5d8b022542a4e74251a5f432824318b3539a5a087f8e53d2f135f" + 19 | "9ca47f3bb2e10aff0af0849504fb7cea3ff192dc8de0edad64c68efde34c56d302ad55fd6e80f3" + 20 | "02d5efcdeae953658d3452561b5f36c542efdbdd9f888538d374cef106acf7d93a4445c3c73cd9" + 21 | "11f0571aaf3d54da12b11ddec375b3"; 22 | #else 23 | public const string PUBLIC_KEY = ""; 24 | #endif 25 | } 26 | -------------------------------------------------------------------------------- /src/ResultEncoding/BasicEncoders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Microsoft.Extensions.Logging; 6 | using Newtonsoft.Json; 7 | 8 | namespace Microsoft.Jupyter.Core 9 | { 10 | 11 | public class JsonResultEncoder : IResultEncoder 12 | { 13 | private readonly ILogger logger; 14 | private readonly JsonConverter[] converters; 15 | private readonly string mimeType; 16 | 17 | public string MimeType => mimeType; 18 | 19 | public JsonResultEncoder(ILogger logger = null, JsonConverter[] converters = null, string mimeType = null) 20 | { 21 | this.logger = logger; 22 | this.converters = converters ?? new JsonConverter[] {}; 23 | this.mimeType = mimeType ?? MimeTypes.Json; 24 | } 25 | 26 | public EncodedData? Encode(object displayable) 27 | { 28 | if (displayable == null) return null; 29 | 30 | try 31 | { 32 | var serialized = JsonConvert.SerializeObject(displayable, converters); 33 | return serialized.ToEncodedData(); 34 | } 35 | catch (Exception ex) 36 | { 37 | logger?.LogWarning(ex, "Failed to serialize display data of type {Type}.", displayable.GetType().ToString()); 38 | return null; 39 | } 40 | } 41 | } 42 | 43 | public class PlainTextResultEncoder : IResultEncoder 44 | { 45 | public string MimeType => MimeTypes.PlainText; 46 | public EncodedData? Encode(object displayable) => 47 | Extensions.ToEncodedData(displayable?.ToString()); 48 | } 49 | 50 | public class ListToTextResultEncoder : IResultEncoder 51 | { 52 | public string MimeType => MimeTypes.PlainText; 53 | public EncodedData? Encode(object displayable) 54 | { 55 | if (displayable == null) return null; 56 | 57 | if (displayable is string) 58 | { 59 | return null; 60 | } 61 | else if (displayable is IEnumerable enumerable) 62 | { 63 | return String.Join(", ", 64 | enumerable.Cast().Select(item => item.ToString()) 65 | ).ToEncodedData(); 66 | } 67 | else return null; 68 | } 69 | } 70 | 71 | public class ListToHtmlResultEncoder : IResultEncoder 72 | { 73 | public string MimeType => MimeTypes.Html; 74 | public EncodedData? Encode(object displayable) 75 | { 76 | if (displayable == null) return null; 77 | 78 | if (displayable is string) 79 | { 80 | return null; 81 | } 82 | else if (displayable is IEnumerable enumerable) 83 | { 84 | var list = String.Join("", 85 | from object item in enumerable 86 | select $"
  • {item}
  • " 87 | ); 88 | return $"
      {list}
    ".ToEncodedData(); 89 | } 90 | else return null; 91 | } 92 | } 93 | 94 | public class FuncResultEncoder : IResultEncoder 95 | { 96 | private Func encode; 97 | private readonly string mimeType; 98 | 99 | public string MimeType => mimeType; 100 | 101 | public FuncResultEncoder(string mimeType, Func encode) 102 | { 103 | this.mimeType = mimeType; 104 | this.encode = encode; 105 | } 106 | 107 | public FuncResultEncoder(string mimeType, Func encode) 108 | : this(mimeType, displayable => encode(displayable).ToEncodedData()) {} 109 | 110 | public EncodedData? Encode(object displayable) 111 | { 112 | if (displayable == null) return null; 113 | return encode(displayable); 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/ResultEncoding/DisplayData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Microsoft.Extensions.Logging; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace Microsoft.Jupyter.Core 10 | { 11 | 12 | public static class MimeTypes 13 | { 14 | public const string PlainText = "text/plain"; 15 | public const string Json = "application/json"; 16 | public const string Html = "text/html"; 17 | public const string Markdown = "text/markdown"; 18 | } 19 | 20 | /// 21 | /// Represents a Jupyter protocol MIME bundle, a pair of dictionaries 22 | /// keyed by MIME types. 23 | /// 24 | /// 25 | /// Both the data and metadata dictionaries are string-valued, even though 26 | /// the Jupyter protocol allows for arbitrary JSON objects with strings 27 | /// being a special case. We adopt this restriction as some clients (in 28 | /// particular, jupyter_client) do not properly handle the more general case. 29 | /// 30 | internal struct MimeBundle 31 | { 32 | public Dictionary Data; 33 | public Dictionary> Metadata; 34 | 35 | public static MimeBundle Empty() => 36 | new MimeBundle 37 | { 38 | Data = new Dictionary(), 39 | Metadata = new Dictionary>() 40 | }; 41 | } 42 | 43 | public struct EncodedData 44 | { 45 | public string Data; 46 | public Dictionary Metadata; 47 | } 48 | 49 | public interface IResultEncoder 50 | { 51 | string MimeType { get; } 52 | EncodedData? Encode(object displayable); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/ResultEncoding/SymbolEncoder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Markdig; 5 | 6 | namespace Microsoft.Jupyter.Core 7 | { 8 | public class MagicSymbolToTextResultEncoder : IResultEncoder 9 | { 10 | public string MimeType => MimeTypes.PlainText; 11 | 12 | public EncodedData? Encode(object displayable) 13 | { 14 | if (displayable is MagicSymbol symbol) 15 | { 16 | return $"{symbol.Name}:\n{symbol.Documentation.Summary ?? ""}" 17 | .ToEncodedData(); 18 | } 19 | else return null; 20 | } 21 | } 22 | 23 | internal static class MarkdownExtensions 24 | { 25 | public static string ToMarkdownHtml(this string markdown, string heading) => 26 | !string.IsNullOrEmpty(markdown) 27 | ? heading + Markdown.ToHtml(markdown) 28 | : string.Empty; 29 | 30 | public static string ToMarkdownHtml(this IEnumerable markdown, string heading) => 31 | markdown != null && markdown.Any() 32 | ? string.Join(string.Empty, markdown.Select(m => m.ToMarkdownHtml(heading))) 33 | : string.Empty; 34 | } 35 | 36 | public class MagicSymbolToHtmlResultEncoder : IResultEncoder 37 | { 38 | internal readonly ImmutableDictionary 39 | Icons = new Dictionary 40 | { 41 | [SymbolKind.Magic] = "fa-magic", 42 | [SymbolKind.Callable] = "fa-terminal", 43 | [SymbolKind.LocalDeclaration] = "fa-stream", 44 | [SymbolKind.Other] = "fa-code" 45 | }.ToImmutableDictionary(); 46 | 47 | public string MimeType => MimeTypes.Html; 48 | 49 | public EncodedData? Encode(object displayable) 50 | { 51 | if (displayable is MagicSymbol symbol) 52 | { 53 | 54 | return ( 55 | $"

    {symbol.Name}

    " + 56 | $"

    {symbol.Documentation.Summary ?? ""}

    " + 57 | symbol.Documentation.Description.ToMarkdownHtml("
    Description
    ") + 58 | symbol.Documentation.Remarks.ToMarkdownHtml("
    Remarks
    ") + 59 | symbol.Documentation.Examples.ToMarkdownHtml("
    Example
    ") 60 | ).ToEncodedData(); 61 | 62 | } 63 | else return null; 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/ResultEncoding/Table.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Newtonsoft.Json; 6 | 7 | namespace Microsoft.Jupyter.Core 8 | { 9 | /// 10 | /// Specifies the text alignment to use for a table cell. 11 | /// 12 | public enum TableCellAlignment 13 | { 14 | /// 15 | /// Align text to the left of the cell. 16 | /// 17 | Left, 18 | 19 | /// 20 | /// Align text to the right of the cell. 21 | /// 22 | Right, 23 | 24 | /// 25 | /// No alignment specified. 26 | /// 27 | None 28 | } 29 | 30 | public static class TableCellAlignmentExtensions 31 | { 32 | /// 33 | /// Constructs an HTML style attribute implementing the specified . 34 | /// 35 | /// The desired cell alignment. 36 | /// 37 | /// A string containing the full HTML style attribute name and value, or an empty string 38 | /// if the alignment is specified as . 39 | /// 40 | /// 41 | /// This method uses start and end values for the text-align CSS attribute, 42 | /// which respect the reading direction (LTR or RTL) of the surrounding HTML. For example, if the 43 | /// specified alignment is , the text will be aligned to the left 44 | /// in LTR and to the right in RTL. 45 | /// 46 | public static string ToStyleAttribute(this TableCellAlignment alignment) => alignment switch 47 | { 48 | TableCellAlignment.Left => "style=\"text-align: start;\"", 49 | TableCellAlignment.Right => "style=\"text-align: end;\"", 50 | _ => string.Empty 51 | }; 52 | 53 | /// 54 | /// Delegates to either or , depending 55 | /// on the specified value. 56 | /// 57 | /// A string representing the table cell contents. 58 | /// The total width of the cell, in characters. 59 | /// Specifies the desired text alignment in the cell 60 | /// 61 | /// The padded string returned from or . 62 | /// 63 | /// 64 | /// If the specified alignment is , the text will be aligned to the left. 65 | /// 66 | public static string PadCell(this string cell, int totalWidth, TableCellAlignment alignment) => alignment switch 67 | { 68 | TableCellAlignment.Left => cell.PadRight(totalWidth), 69 | TableCellAlignment.Right => cell.PadLeft(totalWidth), 70 | _ => cell.PadRight(totalWidth) 71 | }; 72 | } 73 | 74 | public interface ITable 75 | { 76 | string[] Headers { get; } 77 | string[][] Cells { get; } 78 | } 79 | 80 | public class Table : ITable 81 | { 82 | [JsonIgnore] 83 | public List<(string, Func)> Columns; 84 | 85 | [JsonProperty("rows")] 86 | public List Rows; 87 | 88 | [JsonIgnore] 89 | public string[] Headers => 90 | Columns 91 | .Select(header => header.Item1) 92 | .ToArray(); 93 | 94 | [JsonIgnore] 95 | public string[][] Cells => 96 | Rows 97 | .Select( 98 | row => Columns 99 | .Select(column => column.Item2(row)) 100 | .ToArray() 101 | ) 102 | .ToArray(); 103 | } 104 | 105 | public class TableToHtmlDisplayEncoder : IResultEncoder 106 | { 107 | public string MimeType => MimeTypes.Html; 108 | 109 | public TableCellAlignment TableCellAlignment { get; set; } = TableCellAlignment.Left; 110 | 111 | public EncodedData? Encode(object displayable) 112 | { 113 | if (displayable is ITable table) 114 | { 115 | var headers = table.Headers; 116 | var cells = table.Cells; 117 | 118 | return ( 119 | "" + 120 | "" + 121 | "" + 122 | String.Join("", 123 | headers.Select( 124 | header => $"" 125 | ) 126 | ) + 127 | "" + 128 | "" + 129 | "" + 130 | String.Join("", 131 | cells.Select(row => 132 | "" + 133 | String.Join("", 134 | row.Select( 135 | cell => $"" 136 | ) 137 | ) + 138 | "" 139 | ) 140 | ) + 141 | "" + 142 | "
    {header}
    {cell}
    " 143 | ).ToEncodedData(); 144 | } else return null; 145 | } 146 | } 147 | 148 | public class TableToTextDisplayEncoder : IResultEncoder 149 | { 150 | public string MimeType => MimeTypes.PlainText; 151 | 152 | public TableCellAlignment TableCellAlignment { get; set; } = TableCellAlignment.Left; 153 | 154 | public EncodedData? Encode(object displayable) 155 | { 156 | if (displayable is ITable table) 157 | { 158 | var headers = table.Headers; 159 | var cells = table.Cells; 160 | 161 | // For the text, we need to find how wide each column is. 162 | var widths = headers.Select(column => 0).ToArray(); 163 | var nCols = widths.Length; 164 | foreach (var row in cells) 165 | { 166 | foreach (var idx in Enumerable.Range(0, nCols)) 167 | { 168 | if (row[idx].Length > widths[idx]) 169 | { 170 | widths[idx] = row[idx].Length; 171 | } 172 | } 173 | } 174 | 175 | var text = new StringBuilder(); 176 | text.Append(String.Join(" ", 177 | headers.Select((header, idxCol) => 178 | header.PadCell(widths[idxCol], TableCellAlignment) 179 | ) 180 | ).TrimEnd()); 181 | text.Append(Environment.NewLine); 182 | text.Append(String.Join(" ", 183 | widths.Select(width => new String('-', width)) 184 | ).TrimEnd()); 185 | text.Append(Environment.NewLine); 186 | foreach (var row in cells) 187 | { 188 | text.Append(String.Join(" ", 189 | row.Select( 190 | (cell, idxCol) => cell.PadCell(widths[idxCol], TableCellAlignment) 191 | ) 192 | ).TrimEnd()); 193 | text.Append(Environment.NewLine); 194 | } 195 | 196 | 197 | return text.ToString().ToEncodedData(); 198 | } else return null; 199 | } 200 | } 201 | } -------------------------------------------------------------------------------- /src/Servers/HeartbeatServer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Threading; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | using NetMQ; 9 | using NetMQ.Sockets; 10 | 11 | namespace Microsoft.Jupyter.Core 12 | { 13 | public class HeartbeatServer : IDisposable, IHeartbeatServer 14 | { 15 | private bool alive = false; 16 | private Thread thread; 17 | private ResponseSocket socket; 18 | 19 | private ILogger logger; 20 | private KernelContext context; 21 | 22 | public HeartbeatServer( 23 | ILogger logger, 24 | IOptions context 25 | ) 26 | { 27 | this.logger = logger; 28 | this.context = context.Value; 29 | } 30 | 31 | public void Start() 32 | { 33 | alive = true; 34 | thread = new Thread(EventLoop); 35 | thread.Start(); 36 | } 37 | 38 | public void Join() => thread.Join(); 39 | 40 | public void Stop() 41 | { 42 | alive = false; 43 | thread.Interrupt(); 44 | socket?.Close(); 45 | Join(); 46 | thread = null; 47 | } 48 | 49 | private void EventLoop() 50 | { 51 | var addr = context.ConnectionInfo.HeartbeatZmqAddress; 52 | this.logger.LogDebug("Starting heartbeat server at {Address}.", addr); 53 | socket = new ResponseSocket(); 54 | socket.Bind(addr); 55 | 56 | while (alive) 57 | { 58 | // We use the Bytes receiver so that we can ping back data 59 | // unmodified, without worrying about encodings. 60 | try 61 | { 62 | var data = socket.ReceiveFrameBytes(); 63 | logger.LogDebug($"Got heartbeat message of length {data.Length}."); 64 | if (!socket.TrySendFrame(data)) 65 | { 66 | logger.LogError("Error sending heartbeat message back to client."); 67 | } 68 | } 69 | catch (ThreadInterruptedException) 70 | { 71 | continue; 72 | } 73 | catch (Exception ex) 74 | { 75 | logger.LogCritical(ex, "Unhandled exception in heartbeat loop."); 76 | } 77 | } 78 | } 79 | 80 | #region IDisposable Support 81 | private bool disposedValue = false; // To detect redundant calls 82 | 83 | protected virtual void Dispose(bool disposing) 84 | { 85 | if (!disposedValue) 86 | { 87 | if (disposing) 88 | { 89 | socket?.Dispose(); 90 | } 91 | disposedValue = true; 92 | } 93 | } 94 | 95 | // This code added to correctly implement the disposable pattern. 96 | public void Dispose() 97 | { 98 | // Do not change this code. Put cleanup code in Dispose(bool disposing) above. 99 | Dispose(true); 100 | } 101 | #endregion 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Servers/IHeartbeatServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Microsoft.Jupyter.Core 6 | { 7 | public interface IHeartbeatServer 8 | { 9 | void Start(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Servers/IShellServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Jupyter.Core.Protocol; 5 | 6 | namespace Microsoft.Jupyter.Core 7 | { 8 | public interface IShellServer 9 | { 10 | event Action KernelInfoRequest; 11 | 12 | event Action ShutdownRequest; 13 | 14 | void SendShellMessage(Message message); 15 | 16 | void SendIoPubMessage(Message message); 17 | 18 | void Start(); 19 | } 20 | 21 | public interface IShellServerSupportsInterrupt : IShellServer 22 | { 23 | event Action InterruptRequest; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ShellRouting/CommsRouter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Jupyter.Core.Protocol; 11 | using Newtonsoft.Json.Linq; 12 | 13 | #nullable enable 14 | 15 | namespace Microsoft.Jupyter.Core 16 | { 17 | /// 18 | public class CommsRouter : ICommsRouter 19 | { 20 | 21 | #region Inner Classes 22 | 23 | /// 24 | public class CommSession : ICommSession, IDisposable 25 | { 26 | /// 27 | public event Func? OnMessage; 28 | 29 | /// 30 | public event Func? OnClose; 31 | 32 | 33 | /// 34 | public bool IsValid { get; private set; } 35 | 36 | /// 37 | public string Id { get; private set; } 38 | 39 | private readonly CommsRouter commsRouter; 40 | 41 | 42 | internal CommSession(CommsRouter commsRouter, string id) 43 | { 44 | this.IsValid = true; 45 | this.commsRouter = commsRouter; 46 | this.Id = id; 47 | } 48 | 49 | /// 50 | public async Task SendMessage(object contents) 51 | { 52 | if (!IsValid) 53 | { 54 | throw new ProtocolViolationException( 55 | "Attempted to send a message on a comms session that has been closed or disposed. " + 56 | "If you did not close this comms session, it may have been closed from the client. " + 57 | "You can subscribe to the OnClose event of this session to check for closures coming from the client." 58 | ); 59 | } 60 | 61 | var messsage = new Message 62 | { 63 | Header = new MessageHeader 64 | { 65 | MessageType = "comm_msg" 66 | }, 67 | Content = new CommMessageContent 68 | { 69 | Id = this.Id, 70 | RawData = JToken.FromObject(contents) 71 | } 72 | }; 73 | await this.commsRouter.SendMessage(messsage); 74 | } 75 | 76 | internal async Task HandleClose(CommSessionClosedBy closedBy) 77 | { 78 | Debug.Assert(IsValid); 79 | IsValid = false; 80 | await (this.OnClose?.Invoke(closedBy) ?? Task.CompletedTask); 81 | } 82 | 83 | internal async Task HandleMessage(CommMessageContent content) 84 | { 85 | Debug.Assert(IsValid); 86 | await (this.OnMessage?.Invoke(content) ?? Task.CompletedTask); 87 | } 88 | 89 | /// 90 | public async Task Close() 91 | { 92 | // Make Close idempotent; that is, closing a closed session should 93 | // do nothing. 94 | if (!IsValid) 95 | { 96 | return; 97 | } 98 | 99 | var message = new Message 100 | { 101 | Header = new MessageHeader 102 | { 103 | MessageType = "comm_close" 104 | }, 105 | Content = new CommCloseContent 106 | { 107 | Id = this.Id 108 | } 109 | }; 110 | 111 | await this.commsRouter.SendMessage(message); 112 | await HandleClose(closedBy: CommSessionClosedBy.Kernel); 113 | } 114 | 115 | void IDisposable.Dispose() => Close().Wait(); 116 | } 117 | 118 | private class CommSessionOpen : ICommSessionOpen 119 | { 120 | public event Func? On = null; 121 | 122 | internal async Task Handle(ICommSession session, JToken data) => 123 | await (On?.Invoke(session, data) ?? Task.CompletedTask); 124 | } 125 | 126 | #endregion 127 | 128 | private readonly Dictionary openSessions 129 | = new Dictionary(); 130 | 131 | private readonly Dictionary sessionHandlers 132 | = new Dictionary(); 133 | 134 | private IShellServer Server { get; set; } 135 | 136 | private IShellRouter Router { get; set; } 137 | 138 | private ILogger? logger; 139 | 140 | /// 141 | /// Constructs a new comms router, given services required by the 142 | /// new router. 143 | /// 144 | public CommsRouter(IShellServer server, IShellRouter router, ILogger? logger = null) 145 | { 146 | this.Server = server; 147 | this.Router = router; 148 | this.logger = logger; 149 | 150 | router.RegisterHandler("comm_open", async (message) => 151 | { 152 | if (message.Content is CommOpenContent openContent) 153 | { 154 | if (sessionHandlers.TryGetValue(openContent.TargetName, out var handler)) 155 | { 156 | var session = new CommSession(this, openContent.Id); 157 | openSessions.Add(session.Id, session); 158 | await handler.Handle(session, openContent.RawData); 159 | } 160 | else 161 | { 162 | // According to the Jupyter messaging protocol, we are 163 | // supposed to ignore comm_open messages entirely if 164 | // we don't recognizer the target_name property. 165 | logger.LogWarning( 166 | "Got a comm_open message for target name {TargetName}, but no handler for that target name has been registered.", 167 | openContent.TargetName 168 | ); 169 | } 170 | } 171 | else 172 | { 173 | logger.LogError( 174 | "Expected message content for a comm_open message, but got content of type {Type} instead.", 175 | message.Content.GetType() 176 | ); 177 | } 178 | }); 179 | 180 | router.RegisterHandler("comm_msg", async (message) => 181 | { 182 | if (message.Content is CommMessageContent msgContent) 183 | { 184 | if (!openSessions.TryGetValue(msgContent.Id, out var session)) 185 | { 186 | logger.LogError( 187 | "Got comms message for session {Id}, but no such session is currently open.", 188 | session.Id 189 | ); 190 | } 191 | await session.HandleMessage(msgContent); 192 | } 193 | else 194 | { 195 | logger.LogError( 196 | "Expected message content for a comm_msg message, but got content of type {Type} instead.", 197 | message.Content.GetType() 198 | ); 199 | } 200 | }); 201 | 202 | router.RegisterHandler("comm_close", async (message) => 203 | { 204 | if (message.Content is CommCloseContent closeContent) 205 | { 206 | if (!openSessions.TryGetValue(closeContent.Id, out var session)) 207 | { 208 | logger.LogError( 209 | "Asked by client to close comms session with {Id}, but no such session is currently open.", 210 | session.Id 211 | ); 212 | } 213 | openSessions.Remove(session.Id); 214 | await session.HandleClose(CommSessionClosedBy.Client); 215 | } 216 | else 217 | { 218 | logger.LogError( 219 | "Expected message content for a comm_close message, but got content of type {Type} instead.", 220 | message.Content.GetType() 221 | ); 222 | } 223 | }); 224 | } 225 | 226 | /// 227 | public async Task OpenSession(string targetName, object? data = null) 228 | { 229 | var id = Guid.NewGuid().ToString(); 230 | var commSession = new CommSession(this, id); 231 | openSessions.Add(id, commSession); 232 | 233 | await SendMessage(new Message 234 | { 235 | Content = new CommOpenContent 236 | { 237 | RawData = data == null ? null : JToken.FromObject(data), 238 | Id = id, 239 | TargetName = targetName 240 | }, 241 | Header = new MessageHeader 242 | { 243 | MessageType = "comm_open" 244 | } 245 | }); 246 | 247 | return commSession; 248 | } 249 | 250 | internal void RemoveSession(CommSession session) 251 | { 252 | Debug.Assert( 253 | openSessions.ContainsKey(session.Id), 254 | "Attempted to remove a session that was not still open. " + 255 | "This is an internal error that should never happen." 256 | ); 257 | openSessions.Remove(session.Id); 258 | } 259 | 260 | internal Task SendMessage(Message messsage) 261 | { 262 | this.Server.SendIoPubMessage(messsage); 263 | return Task.CompletedTask; 264 | } 265 | 266 | /// 267 | public ICommSessionOpen SessionOpenEvent(string targetName) 268 | { 269 | if (sessionHandlers.TryGetValue(targetName, out var handler)) 270 | { 271 | return handler; 272 | } 273 | else 274 | { 275 | var newHandler = new CommSessionOpen(); 276 | sessionHandlers.Add(targetName, newHandler); 277 | return newHandler; 278 | } 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/ShellRouting/ICommsRouter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Threading.Tasks; 8 | using Microsoft.Jupyter.Core.Protocol; 9 | using Newtonsoft.Json.Linq; 10 | 11 | namespace Microsoft.Jupyter.Core 12 | { 13 | 14 | public enum CommSessionClosedBy 15 | { 16 | Client, 17 | Kernel, 18 | } 19 | 20 | public interface ICommSession 21 | { 22 | /// 23 | /// Raised when a new message is available from the client. 24 | /// 25 | event Func? OnMessage; 26 | 27 | /// 28 | /// Raised when this session is closed, whether by the kernel or 29 | /// by the client. 30 | /// 31 | event Func? OnClose; 32 | 33 | /// 34 | /// If true, then this session is still open and can be used 35 | /// to send and receive comms messages. 36 | /// 37 | bool IsValid { get; } 38 | 39 | /// 40 | /// A unique ID identifying this session as part of a larger Jupyter 41 | /// messaging session. 42 | /// 43 | string Id { get; } 44 | 45 | /// 46 | /// Sends a comm message to the client. 47 | /// 48 | Task SendMessage(object contents); 49 | 50 | /// 51 | /// Closes this comm session, notifying the client if the session 52 | /// was not already closed. 53 | /// 54 | Task Close(); 55 | } 56 | 57 | public interface ICommSessionOpen 58 | { 59 | event Func? On; 60 | } 61 | 62 | public interface ICommsRouter 63 | { 64 | ICommSessionOpen SessionOpenEvent(string targetName); 65 | 66 | Task OpenSession(string targetName, object? data); 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ShellRouting/IShellHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Jupyter.Core.Protocol; 11 | 12 | namespace Microsoft.Jupyter.Core 13 | { 14 | /// 15 | /// Represents a class that can handle and respond to 16 | /// shell messages coming in from a client. 17 | /// 18 | public interface IShellHandler 19 | { 20 | /// 21 | /// The message type handled by this handler (e.g.: kernel_info_request). 22 | /// 23 | public string MessageType { get; } 24 | 25 | /// 26 | /// Called by the shell server to asynchronously handle a message 27 | /// coming in from the client. Either returns null if no 28 | /// further handling is required, or a task that can be awaited on 29 | /// for the message to be completely handled. 30 | /// 31 | /// 32 | /// The incoming message to be handled. 33 | /// 34 | public Task HandleAsync(Message message); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ShellRouting/IShellRouter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Jupyter.Core.Protocol; 11 | 12 | namespace Microsoft.Jupyter.Core 13 | { 14 | /// 15 | /// Represents a class that can be used to route incoming shell 16 | /// messages to appropriate handlers. 17 | /// 18 | public interface IShellRouter 19 | { 20 | 21 | /// 22 | /// Registers an action that can be used to handle a 23 | /// particular message type. This action either returns a task 24 | /// that can be awaited if further processing is required, or 25 | /// null if the handler has completed handling the message. 26 | /// 27 | /// 28 | /// The type of message to be handled. 29 | /// 30 | /// 31 | /// An action that can be used to handle incoming messages of 32 | /// type messageType. 33 | /// 34 | /// 35 | /// To register a handler that logs ping_request 36 | /// message IDs: 37 | /// logger.LogDebug( 41 | /// "Got ping_request with id {Id}.", 42 | /// message.Header.Id 43 | /// ) 44 | /// ); 45 | /// ]]> 46 | /// 47 | public void RegisterHandler(string messageType, Func handler); 48 | 49 | public THandler RegisterHandler(IServiceProvider serviceProvider) 50 | where THandler: IShellHandler 51 | { 52 | var handler = (THandler)ActivatorUtilities.CreateInstance(serviceProvider, typeof(THandler)); 53 | RegisterHandler(handler); 54 | return handler; 55 | } 56 | 57 | /// 58 | /// Registers a handler that can be used to handle a 59 | /// particular message type. 60 | /// 61 | /// 62 | /// The handler to be registered; the 63 | /// 64 | /// property of the handler will be used to define the 65 | /// message type to be handled. 66 | /// 67 | public void RegisterHandler(IShellHandler handler) => 68 | RegisterHandler(handler.MessageType, handler.HandleAsync); 69 | 70 | /// 71 | /// Registers an action to be used to handle messages 72 | /// whose message types do not have appropriate handlers. 73 | /// 74 | /// 75 | /// An action that can be used to handle messages 76 | /// whose message types do not have appropriate handlers. 77 | /// 78 | public void RegisterFallback(Func fallback); 79 | 80 | /// 81 | /// Calls the appropriate handler for a given message, 82 | /// or the fallback handler if no more appropriate handler 83 | /// exists. 84 | /// 85 | /// 86 | /// The message to be handled. 87 | /// 88 | public Task? Handle(Message message); 89 | 90 | /// 91 | /// Searches an assembly for types representing shell 92 | /// handlers and registers each handler. 93 | /// 94 | /// 95 | /// A type from the assembly to be searched. 96 | /// 97 | public void RegisterHandlers(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ShellRouting/OrderedShellHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Jupyter.Core.Protocol; 12 | 13 | namespace Microsoft.Jupyter.Core 14 | { 15 | public abstract class OrderedShellHandler : IShellHandler 16 | where TResult: struct 17 | { 18 | private Task? currentTask = null; 19 | 20 | private int taskDepth = 0; 21 | 22 | protected virtual ILogger? Logger { get; set; } = null; 23 | 24 | public abstract string MessageType { get; } 25 | public abstract Task HandleAsync(Message message, TResult? previousResult); 26 | public virtual Task HandleAsync(Message message, TResult? previousResult, Action onHandled) => 27 | HandleAsync(message, previousResult); 28 | 29 | public Task HandleAsync(Message message) 30 | { 31 | Logger?.LogDebug("Handing {MessageType} with ordered shell handler.", message.Header.MessageType); 32 | currentTask = new Task((state) => 33 | { 34 | taskDepth++; 35 | var previousTask = (Task?)state; 36 | var previousResult = previousTask?.Result; 37 | 38 | var handled = false; 39 | Action onHandled = () => 40 | { 41 | handled = true; 42 | taskDepth--; 43 | if (taskDepth == 0) 44 | { 45 | currentTask = null; 46 | } 47 | }; 48 | var currentResult = HandleAsync(message, previousResult, onHandled).Result; 49 | if (!handled) 50 | { 51 | onHandled(); 52 | } 53 | return currentResult; 54 | }, currentTask); 55 | currentTask.Start(); 56 | return currentTask; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ShellRouting/ShellRouter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Jupyter.Core.Protocol; 13 | 14 | namespace Microsoft.Jupyter.Core 15 | { 16 | 17 | /// 18 | /// Routes shell messages to handlers based on the message type 19 | /// of each incoming shell messsge. 20 | /// 21 | public class ShellRouter : IShellRouter 22 | { 23 | private readonly IDictionary> shellHandlers 24 | = new Dictionary>(); 25 | private Func? fallback; 26 | private readonly ILogger logger; 27 | private IServiceProvider services; 28 | 29 | public ShellRouter( 30 | IServiceProvider services, 31 | ILogger logger 32 | ) 33 | { 34 | if (services == null) { throw new ArgumentNullException(nameof(services)); } 35 | if (logger == null) { throw new ArgumentNullException(nameof(logger)); } 36 | this.logger = logger; 37 | this.services = services; 38 | 39 | // Set a default fallback action. 40 | RegisterFallback(async message => 41 | logger.LogWarning( 42 | "Unrecognized custom shell message of type {Type}: {Message}", 43 | message.Header.MessageType, 44 | message 45 | ) 46 | ); 47 | } 48 | 49 | public Task? Handle(Message message) 50 | { 51 | logger.LogDebug("Handling message of type {MessageType}.", message.Header.MessageType); 52 | return ( 53 | shellHandlers.TryGetValue(message.Header.MessageType, out var handler) 54 | ? handler : fallback 55 | )?.Invoke(message); 56 | } 57 | 58 | public void RegisterHandler(string messageType, Func handler) 59 | { 60 | shellHandlers[messageType] = handler; 61 | } 62 | 63 | /// 64 | public void RegisterFallback(Func fallback) => 65 | this.fallback = fallback; 66 | 67 | /// 68 | public void RegisterHandlers() 69 | { 70 | var handlers = typeof(TAssembly) 71 | .Assembly 72 | .GetTypes() 73 | .Where(t => 74 | { 75 | if (!t.IsClass && t.IsAbstract) { return false; } 76 | var matched = t 77 | .GetInterfaces() 78 | .Contains(typeof(IShellHandler)); 79 | this.logger.LogDebug("Class {Class} subclass of CustomShellHandler? {Matched}", t.FullName, matched); 80 | return matched; 81 | }) 82 | .Select(handlerType => 83 | ActivatorUtilities.CreateInstance(services, handlerType) 84 | ) 85 | .Cast(); 86 | 87 | foreach (var handler in handlers) 88 | { 89 | logger.LogInformation("Registering handler for type: {Type}", handler.MessageType); 90 | ((IShellRouter) this).RegisterHandler(handler); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Symbols/ISymbolResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | #nullable enable 4 | 5 | namespace Microsoft.Jupyter.Core 6 | { 7 | /// 8 | /// Identifies the kind of a symbol (e.g.: whether a symbol is a magic 9 | /// symbol). 10 | /// 11 | public enum SymbolKind 12 | { 13 | /// 14 | /// Indicates that a symbol is a magic symbol, and not a part of 15 | /// the language supported by an execution engine. 16 | /// 17 | Magic, 18 | 19 | /// 20 | /// Indicates that a symbol represents a callable function, 21 | /// operation, method, or similar. 22 | /// 23 | Callable, 24 | 25 | /// 26 | /// Indicates that a symbol represents a local declaration, 27 | /// variable, or similar. 28 | /// 29 | LocalDeclaration, 30 | 31 | /// 32 | /// Indicates that a symbol is does not belong to any other kinds 33 | /// listed in this enum. 34 | /// 35 | Other 36 | } 37 | 38 | /// 39 | /// Documentation for a symbol as resolved by an execution engine. 40 | /// 41 | public struct Documentation 42 | { 43 | /// 44 | /// Summary for the documented symbol. Should be at most a 45 | /// sentence or two. 46 | /// 47 | public string Summary; 48 | 49 | [Obsolete("Deprecated, please break into more specific fields as appropriate.")] 50 | public string? Full; 51 | 52 | /// 53 | /// A detailed description of the documented symbol, formatted as 54 | /// a Markdown document. 55 | /// 56 | /// 57 | /// This Markdown document should not contain H1 or H2 headers. 58 | /// 59 | public string? Description; 60 | 61 | /// 62 | /// Additional remarks about the documented symbol, formatted as 63 | /// a Markdown document. 64 | /// 65 | /// 66 | /// This Markdown document should not contain H1 or H2 headers. 67 | /// 68 | public string? Remarks; 69 | 70 | /// 71 | /// Examples of how to use the documented symbol, formatted as 72 | /// a sequence of Markdown documents. 73 | /// 74 | /// 75 | /// The Markdown documents in this field should not contain H1 or 76 | /// H2 headers. 77 | /// 78 | public IEnumerable? Examples; 79 | 80 | /// 81 | /// Additional links relevant to the documented symbol, formatted 82 | /// as a sequence of links, each with a description and a target. 83 | /// 84 | public IEnumerable<(string, Uri)>? SeeAlso; 85 | } 86 | 87 | /// 88 | /// Represents a symbol that can be resolved by a symbol resolver, 89 | /// such as a magic symbol, a completion result, or so forth. 90 | /// 91 | public interface ISymbol 92 | { 93 | /// 94 | /// The name of the resolved symbol. 95 | /// 96 | string Name { get; } 97 | 98 | /// 99 | /// The kind of the resolved symbol (e.g. is it a magic command, 100 | /// a local variable, a function, etc.). 101 | /// 102 | SymbolKind Kind { get; } 103 | } 104 | 105 | /// 106 | /// A service that can be used to resolve symbols from symbol names. 107 | /// 108 | public interface ISymbolResolver 109 | { 110 | /// 111 | /// Resolves a global symbol. 112 | /// 113 | /// 114 | /// The resolved symbol, or null if no such symbol can be 115 | /// successfully resolved. 116 | /// 117 | ISymbol? Resolve(string symbolName); 118 | 119 | /// 120 | /// Resolves a global symbol into possible matches, given a prefix. 121 | /// 122 | /// 123 | /// An enumeration of resolved symbols matching the given prefix, 124 | /// if any, or an empty enumeration if this resolver does not 125 | /// support prefix matching. 126 | /// 127 | IEnumerable MaybeResolvePrefix(string symbolPrefix) => 128 | Array.Empty(); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/Symbols/Magic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Newtonsoft.Json; 9 | 10 | namespace Microsoft.Jupyter.Core 11 | { 12 | 13 | /// 14 | /// Marks that a given method implements the given magic command. 15 | /// 16 | /// 17 | /// Each magic command method must have the signature 18 | /// Task<ExecutionResult> (string, IChannel), similar to 19 | /// BaseEngine.ExecuteMundane. 20 | /// 21 | [System.AttributeUsage(System.AttributeTargets.Method)] 22 | public class MagicCommandAttribute : System.Attribute 23 | { 24 | /// 25 | /// Name of the magic command represented by this method. 26 | /// 27 | public readonly string Name; 28 | 29 | /// 30 | /// Documentation to be presented to the user in repsonse to a 31 | /// help command. 32 | /// 33 | public readonly Documentation Documentation; 34 | 35 | /// 36 | /// Constructs a new attribute that marks a given method as 37 | /// implementing a given magic command. 38 | /// 39 | public MagicCommandAttribute( 40 | string name, 41 | string summary, 42 | string fullDocumentation = null 43 | ) 44 | { 45 | Name = name; 46 | Documentation = new Documentation 47 | { 48 | Full = fullDocumentation, 49 | Summary = summary 50 | }; 51 | } 52 | } 53 | 54 | /// 55 | /// A symbol representing a magic command. 56 | /// 57 | public class MagicSymbol : ISymbol 58 | { 59 | /// 60 | public string Name { get; set; } 61 | 62 | /// 63 | public SymbolKind Kind { get; set; } 64 | 65 | /// 66 | /// Documentation about this magic command to be displayed to the user. 67 | /// 68 | public Documentation Documentation { get; set; } 69 | 70 | /// 71 | /// A function to be run when the magic command is executed by the 72 | /// user. 73 | /// 74 | [JsonIgnore] 75 | public Func> Execute { get; set; } 76 | } 77 | 78 | /// 79 | /// A symbol representing a magic command that is cancellable. 80 | /// 81 | public class CancellableMagicSymbol : MagicSymbol 82 | { 83 | /// 84 | /// Creates a cancellable magic symbol object. 85 | /// 86 | public CancellableMagicSymbol() => 87 | this.Execute = (input, channel) => this.ExecuteCancellable(input, channel, CancellationToken.None); 88 | 89 | /// 90 | /// A function to be run when the magic command is executed by the 91 | /// user which supports cancellation. 92 | /// 93 | [JsonIgnore] 94 | public Func> ExecuteCancellable { get; set; } 95 | } 96 | 97 | /// 98 | /// A symbol resolver that uses 99 | /// attributes to find magic commands in a given engine class. 100 | /// 101 | public class MagicCommandResolver : ISymbolResolver 102 | { 103 | private IExecutionEngine engine; 104 | private IDictionary methods; 105 | 106 | /// 107 | /// Constructs a new resolver by searching a given engine for 108 | /// methods annotated with the 109 | /// attribute. 110 | /// 111 | /// 112 | /// The execution engine to be searched for magic command methods. 113 | /// 114 | public MagicCommandResolver(IExecutionEngine engine) 115 | { 116 | this.engine = engine; 117 | methods = engine 118 | .GetType() 119 | .GetMethods() 120 | .Where( 121 | method => method.GetCustomAttributes(typeof(MagicCommandAttribute), inherit: true).Length > 0 122 | ) 123 | .Select( 124 | method => { 125 | var attr = ( 126 | (MagicCommandAttribute) 127 | method 128 | .GetCustomAttributes(typeof(MagicCommandAttribute), inherit: true) 129 | .Single() 130 | ); 131 | return (attr, method); 132 | } 133 | ) 134 | .ToImmutableDictionary( 135 | pair => pair.attr.Name, 136 | pair => (pair.attr, pair.method) 137 | ); 138 | 139 | } 140 | 141 | private ISymbol SymbolForMethod(MagicCommandAttribute attr, MethodInfo method) => 142 | new MagicSymbol 143 | { 144 | Name = attr.Name, 145 | Documentation = attr.Documentation, 146 | Kind = SymbolKind.Magic, 147 | Execute = (input, channel) => 148 | { 149 | try 150 | { 151 | return (Task)(method.Invoke(engine, new object[] { input, channel })); 152 | } 153 | catch (TargetInvocationException e) 154 | { 155 | throw e.InnerException; 156 | } 157 | catch (Exception) 158 | { 159 | throw new InvalidOperationException($"Invalid magic method for {attr.Name}. Expecting a public async method that takes a String and and IChannel as parameters."); 160 | } 161 | } 162 | }; 163 | 164 | /// 165 | public ISymbol Resolve(string symbolName) 166 | { 167 | if (this.methods.ContainsKey(symbolName)) 168 | { 169 | (var attr, var method) = this.methods[symbolName]; 170 | return SymbolForMethod(attr, method); 171 | } 172 | else return null; 173 | } 174 | 175 | /// 176 | public IEnumerable MaybeResolvePrefix(string symbolPrefix) 177 | { 178 | foreach (var (key, (attr, method)) in this.methods.Where(item => item.Value.Item1.Name.StartsWith(symbolPrefix))) 179 | { 180 | yield return SymbolForMethod(attr, method); 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/jupyter-core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | netstandard2.1 6 | 8.0 7 | 8 | Microsoft.Jupyter.Core 9 | $(BUILD_MAJOR).$(BUILD_MINOR).0.0 10 | 11 | true 12 | Microsoft 13 | Provides support for writing Jupyter kernels using the .NET Core SDK. 14 | © Microsoft Corporation. All rights reserved. 15 | 16 | https://github.com/Microsoft/jupyter-core/blob/master/LICENSE 17 | https://github.com/Microsoft/jupyter-core/ 18 | true 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/core/EncoderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Text.RegularExpressions; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Microsoft.Extensions.Logging.Console; 7 | using Newtonsoft.Json.Linq; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Microsoft.Jupyter.Core 11 | { 12 | 13 | internal static class TestExtensions 14 | { 15 | public static void IsEmpty(this Assert assert, IEnumerable source) 16 | { 17 | Assert.IsNotNull(source); 18 | Assert.AreEqual(expected: 0, actual: source.Count()); 19 | } 20 | } 21 | 22 | [TestClass] 23 | public class EncoderTests 24 | { 25 | private readonly ITable exampleTable = 26 | new Table<(int, double)> 27 | { 28 | Columns = new List<(string, Func<(int, double), string>)> 29 | { 30 | ("foo", row => row.Item1.ToString()), 31 | ("bar", row => row.Item2.ToString()) 32 | }, 33 | 34 | Rows = new List<(int, double)> 35 | { 36 | (42, 3.14), 37 | (1337, 2.718) 38 | } 39 | }; 40 | 41 | [TestMethod] 42 | public void TestStringToPlainText() 43 | { 44 | var encoder = new PlainTextResultEncoder(); 45 | Assert.AreEqual(encoder.MimeType, MimeTypes.PlainText); 46 | var data = encoder.Encode("foo"); 47 | Assert.IsTrue(data.HasValue); 48 | Assert.AreEqual(data.Value.Data, "foo"); 49 | Assert.That.IsEmpty(data.Value.Metadata); 50 | 51 | data = encoder.Encode(null); 52 | Assert.IsTrue(data.HasValue); 53 | Assert.AreEqual(data.Value.Data, null); 54 | Assert.That.IsEmpty(data.Value.Metadata); 55 | } 56 | 57 | 58 | [TestMethod] 59 | public void TestListToPlainText() 60 | { 61 | var encoder = new ListToTextResultEncoder(); 62 | Assert.AreEqual(encoder.MimeType, MimeTypes.PlainText); 63 | Assert.IsNull(encoder.Encode(null)); 64 | var data = encoder.Encode(new [] {"foo", "bar"}); 65 | Assert.IsTrue(data.HasValue); 66 | Assert.AreEqual(data.Value.Data, "foo, bar"); 67 | Assert.That.IsEmpty(data.Value.Metadata); 68 | } 69 | 70 | [TestMethod] 71 | public void TestListToHtml() 72 | { 73 | var encoder = new ListToHtmlResultEncoder(); 74 | Assert.AreEqual(encoder.MimeType, MimeTypes.Html); 75 | Assert.IsNull(encoder.Encode(null)); 76 | var data = encoder.Encode(new [] {"foo", "bar"}); 77 | Assert.IsTrue(data.HasValue); 78 | Assert.AreEqual(data.Value.Data, "
    • foo
    • bar
    "); 79 | Assert.That.IsEmpty(data.Value.Metadata); 80 | } 81 | 82 | [TestMethod] 83 | public void TestFuncEncoder() 84 | { 85 | var encoder = new FuncResultEncoder( 86 | MimeTypes.PlainText, 87 | displayable => String.Join("", displayable.ToString().Reverse()) 88 | ); 89 | Assert.AreEqual(encoder.MimeType, MimeTypes.PlainText); 90 | Assert.IsNull(encoder.Encode(null)); 91 | var data = encoder.Encode("foo"); 92 | Assert.IsTrue(data.HasValue); 93 | Assert.AreEqual(data.Value.Data, "oof"); 94 | Assert.That.IsEmpty(data.Value.Metadata); 95 | } 96 | 97 | [TestMethod] 98 | public void TestTableToText() 99 | { 100 | var encoder = new TableToTextDisplayEncoder(); 101 | Assert.AreEqual(encoder.MimeType, MimeTypes.PlainText); 102 | Assert.IsNull(encoder.Encode(null)); 103 | var data = encoder.Encode(exampleTable); 104 | Assert.IsTrue(data.HasValue); 105 | var expected = @"foo bar 106 | ---- ----- 107 | 42 3.14 108 | 1337 2.718 109 | "; 110 | Assert.AreEqual(data.Value.Data, expected); 111 | Assert.That.IsEmpty(data.Value.Metadata); 112 | } 113 | 114 | [TestMethod] 115 | public void TestTableToTextRightAlign() 116 | { 117 | var encoder = new TableToTextDisplayEncoder() { TableCellAlignment = TableCellAlignment.Right }; 118 | Assert.AreEqual(encoder.MimeType, MimeTypes.PlainText); 119 | Assert.IsNull(encoder.Encode(null)); 120 | var data = encoder.Encode(exampleTable); 121 | Assert.IsTrue(data.HasValue); 122 | var expected = @" foo bar 123 | ---- ----- 124 | 42 3.14 125 | 1337 2.718 126 | "; 127 | Assert.AreEqual(data.Value.Data, expected); 128 | Assert.That.IsEmpty(data.Value.Metadata); 129 | } 130 | 131 | [TestMethod] 132 | public void TestTableToHtml() 133 | { 134 | foreach (TableCellAlignment alignment in typeof(TableCellAlignment).GetEnumValues()) 135 | { 136 | var encoder = new TableToHtmlDisplayEncoder() { TableCellAlignment = alignment }; 137 | Assert.AreEqual(encoder.MimeType, MimeTypes.Html); 138 | Assert.IsNull(encoder.Encode(null)); 139 | var data = encoder.Encode(exampleTable); 140 | Assert.IsTrue(data.HasValue); 141 | var style = alignment.ToStyleAttribute(); 142 | var expected = 143 | "" + 144 | $"" + 145 | "" + 146 | $"" + 147 | $"" + 148 | "" + 149 | "
    foobar
    423.14
    13372.718
    "; 150 | Assert.AreEqual(expected, data.Value.Data); 151 | Assert.That.IsEmpty(data.Value.Metadata); 152 | } 153 | } 154 | 155 | [TestMethod] 156 | public void TestJsonEncoder() 157 | { 158 | using (var loggingFactory = LoggerFactory.Create(builder => 159 | builder 160 | .AddFilter((name, level) => true) 161 | .AddConsole(options => options.IncludeScopes = true) 162 | )) 163 | { 164 | var encoder = new JsonResultEncoder( 165 | loggingFactory.CreateLogger("test") 166 | ); 167 | Assert.AreEqual(encoder.MimeType, MimeTypes.Json); 168 | Assert.IsNull(encoder.Encode(null)); 169 | var data = encoder.Encode(exampleTable); 170 | var jData = JObject.Parse(data.Value.Data); 171 | Assert.IsTrue(JToken.DeepEquals( 172 | jData, 173 | new JObject 174 | { 175 | {"rows", new JArray 176 | { 177 | new JObject { {"Item1", 42}, {"Item2", 3.14} }, 178 | new JObject { {"Item1", 1337}, {"Item2", 2.718} }, 179 | } 180 | } 181 | } 182 | )); 183 | } 184 | } 185 | 186 | [TestMethod] 187 | public void TestSymbolIcons() 188 | { 189 | var encoder = new MagicSymbolToHtmlResultEncoder(); 190 | Assert.IsNull(encoder.Encode(null)); 191 | 192 | foreach (SymbolKind kind in Enum.GetValues(typeof(SymbolKind))) 193 | { 194 | Assert.IsTrue(encoder.Icons.ContainsKey(kind)); 195 | } 196 | } 197 | 198 | private string EncodeTestMagic(Documentation documentation) 199 | { 200 | var magic = new MagicSymbol 201 | { 202 | Name = "%test", 203 | Documentation = documentation, 204 | Kind = SymbolKind.Magic, 205 | Execute = async (input, channel) => ExecutionResult.Aborted 206 | }; 207 | return new MagicSymbolToHtmlResultEncoder().Encode(magic)?.Data ?? string.Empty; 208 | } 209 | 210 | private void AssertContainsAll(string fullString, params string[] values) => 211 | Assert.IsTrue(values.All(v => fullString.Contains(v))); 212 | 213 | private void AssertContainsNone(string fullString, params string[] values) => 214 | Assert.IsFalse(values.Any(v => fullString.Contains(v))); 215 | 216 | [TestMethod] 217 | public void TestMagicEncoder() 218 | { 219 | var headingDescription = "
    Description
    "; 220 | var headingRemarks = "
    Remarks
    "; 221 | var headingExample = "
    Example
    "; 222 | 223 | var documentation = new Documentation(); 224 | var encoding = EncodeTestMagic(documentation); 225 | AssertContainsNone(encoding, headingDescription, headingRemarks, headingExample); 226 | 227 | documentation.Summary = "Test summary."; 228 | encoding = EncodeTestMagic(documentation); 229 | AssertContainsAll(encoding, documentation.Summary); 230 | AssertContainsNone(encoding, headingDescription, headingRemarks, headingExample); 231 | 232 | documentation.Description = "Test description."; 233 | encoding = EncodeTestMagic(documentation); 234 | AssertContainsAll(encoding, documentation.Summary, documentation.Description, headingDescription); 235 | AssertContainsNone(encoding, headingRemarks, headingExample); 236 | 237 | documentation.Remarks = "Test remarks."; 238 | encoding = EncodeTestMagic(documentation); 239 | AssertContainsAll(encoding, documentation.Summary, documentation.Description, documentation.Remarks, headingDescription, headingRemarks); 240 | AssertContainsNone(encoding, headingExample); 241 | 242 | documentation.Examples = new List(); 243 | encoding = EncodeTestMagic(documentation); 244 | AssertContainsAll(encoding, documentation.Summary, documentation.Description, documentation.Remarks, headingDescription, headingRemarks); 245 | AssertContainsNone(encoding, headingExample); 246 | 247 | documentation.Examples = new List() { "First example.", "Second example." }; 248 | encoding = EncodeTestMagic(documentation); 249 | AssertContainsAll(encoding, documentation.Summary, documentation.Description, documentation.Remarks, headingDescription, headingRemarks); 250 | AssertContainsAll(encoding, documentation.Examples.ToArray()); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /tests/core/InputParserTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | #nullable enable 5 | 6 | using System; 7 | using System.Linq; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | 10 | namespace Microsoft.Jupyter.Core 11 | { 12 | internal class MundaneSymbol : ISymbol 13 | { 14 | public string Name { get; set; } 15 | 16 | public SymbolKind Kind { get; set; } 17 | } 18 | 19 | internal class MockSymbolResolver : ISymbolResolver 20 | { 21 | private string[] magicSymbols = new[] { "%abc", "%def" }; 22 | private string[] otherSymbols = new[] { "ghi", "jkl" }; 23 | 24 | public ISymbol? Resolve(string symbolName) => 25 | this.magicSymbols.Contains(symbolName) 26 | ? new MagicSymbol 27 | { 28 | Name = symbolName, 29 | Documentation = new Documentation(), 30 | Kind = SymbolKind.Magic, 31 | Execute = async (input, channel) => ExecutionResult.Aborted 32 | } as ISymbol 33 | : 34 | this.otherSymbols.Contains(symbolName) 35 | ? new MundaneSymbol 36 | { 37 | Name = symbolName, 38 | Kind = SymbolKind.Callable 39 | } as ISymbol 40 | : 41 | null; 42 | } 43 | 44 | [TestClass] 45 | public class InputParserTests 46 | { 47 | [TestMethod] 48 | public void TestEmptyInput() 49 | { 50 | var input = string.Empty; 51 | 52 | var inputParser = new InputParser(new MockSymbolResolver()); 53 | var commandType = inputParser.GetNextCommand(input, out ISymbol? symbol, out string? commandInput, out string? remainingInput); 54 | 55 | Assert.AreEqual(commandType, InputParser.CommandType.Mundane); 56 | Assert.IsNull(symbol); 57 | Assert.AreEqual(commandInput, string.Empty); 58 | Assert.AreEqual(remainingInput, string.Empty); 59 | } 60 | 61 | [TestMethod] 62 | public void TestInvalidMagic() 63 | { 64 | var inputs = new[] { 65 | "%notamagic", // invalid magic 66 | "% abc", // magic with space 67 | "text %abc", // magic not at the beginning of a line 68 | "%abc%def", // magic without trailing whitespace 69 | }; 70 | 71 | foreach (var input in inputs) 72 | { 73 | var inputParser = new InputParser(new MockSymbolResolver()); 74 | var commandType = inputParser.GetNextCommand(input, out ISymbol? symbol, out string? commandInput, out string? remainingInput); 75 | 76 | Assert.AreEqual(commandType, InputParser.CommandType.Mundane); 77 | Assert.IsNull(symbol, $"Input:\n{input}"); 78 | Assert.AreEqual(commandInput, input, $"Input:\n{input}"); 79 | Assert.AreEqual(remainingInput, string.Empty, $"Input:\n{input}"); 80 | } 81 | } 82 | 83 | [TestMethod] 84 | public void TestSingleHelp() 85 | { 86 | var inputs = new[] { 87 | "?ghi", // simple case with leading ? 88 | "jkl?", // simple case with trailing ? 89 | " \t ?ghi", // leading whitespace should have no impact 90 | " \r\n ?ghi \n arg ", // leading line breaks should have no impact 91 | "?jkl arg \n\t ?xyz arg", // invalid symbol should be treated as plain text 92 | "?ghi arg ?jkl arg", // help in middle of line should not be detected 93 | }; 94 | 95 | foreach (var input in inputs) 96 | { 97 | var inputParser = new InputParser(new MockSymbolResolver()); 98 | var commandType = inputParser.GetNextCommand(input, out ISymbol? symbol, out string? commandInput, out string? remainingInput); 99 | 100 | Assert.AreEqual(commandType, InputParser.CommandType.Help); 101 | Assert.IsNotNull(symbol, $"Input:\n{input}"); 102 | Assert.AreEqual(commandInput, input, $"Input:\n{input}"); 103 | Assert.AreEqual(remainingInput, string.Empty, $"Input:\n{input}"); 104 | } 105 | } 106 | 107 | [TestMethod] 108 | public void TestSingleMagic() 109 | { 110 | var inputs = new[] { 111 | "%abc", // simple case 112 | "%abc arg", // simple case with argument 113 | "%abc ?", // simple case with argument, not help because of whitespace 114 | " \t %abc", // leading whitespace should have no impact 115 | " \r\n %def \n arg ", // leading line breaks should have no impact 116 | "%def arg \n\t %notamagic arg", // invalid magic should be treated as plain text 117 | "%abc arg %def arg", // magic in middle of line should not be detected 118 | }; 119 | 120 | foreach (var input in inputs) 121 | { 122 | var inputParser = new InputParser(new MockSymbolResolver()); 123 | var commandType = inputParser.GetNextCommand(input, out ISymbol? symbol, out string? commandInput, out string? remainingInput); 124 | 125 | Assert.AreEqual(commandType, InputParser.CommandType.Magic); 126 | Assert.IsNotNull(symbol, $"Input:\n{input}"); 127 | Assert.AreEqual(commandInput, input, $"Input:\n{input}"); 128 | Assert.AreEqual(remainingInput, string.Empty, $"Input:\n{input}"); 129 | } 130 | } 131 | 132 | [TestMethod] 133 | public void TestSingleMagicHelp() 134 | { 135 | var inputs = new[] { 136 | "?%abc", // leading ? 137 | "%abc?", // trailing ? 138 | " \r\n %abc? arg \n", // leading whitespace and trailing text 139 | }; 140 | 141 | foreach (var input in inputs) 142 | { 143 | var inputParser = new InputParser(new MockSymbolResolver()); 144 | var commandType = inputParser.GetNextCommand(input, out ISymbol? symbol, out string? commandInput, out string? remainingInput); 145 | 146 | Assert.AreEqual(commandType, InputParser.CommandType.MagicHelp); 147 | Assert.IsNotNull(symbol, $"Input:\n{input}"); 148 | Assert.AreEqual(commandInput, input, $"Input:\n{input}"); 149 | Assert.AreEqual(remainingInput, string.Empty, $"Input:\n{input}"); 150 | } 151 | } 152 | 153 | [TestMethod] 154 | public void TestMultipleMagics() 155 | { 156 | var inputParser = new InputParser(new MockSymbolResolver()); 157 | 158 | // simple case 159 | var input = "%abc\n%def"; 160 | var commandType = inputParser.GetNextCommand(input, out ISymbol? symbol, out string? commandInput, out string? remainingInput); 161 | 162 | Assert.AreEqual(commandType, InputParser.CommandType.Magic); 163 | Assert.IsNotNull(symbol, $"Input:\n{input}"); 164 | Assert.AreEqual(commandInput, "%abc", $"Input:\n{input}"); 165 | Assert.AreEqual(remainingInput, "%def", $"Input:\n{input}"); 166 | 167 | // simple case with help 168 | input = "%abc?\n%def"; 169 | commandType = inputParser.GetNextCommand(input, out symbol, out commandInput, out remainingInput); 170 | 171 | Assert.AreEqual(commandType, InputParser.CommandType.MagicHelp); 172 | Assert.IsNotNull(symbol, $"Input:\n{input}"); 173 | Assert.AreEqual(commandInput, "%abc?", $"Input:\n{input}"); 174 | Assert.AreEqual(remainingInput, "%def", $"Input:\n{input}"); 175 | 176 | // multi-line args and extra whitespace 177 | input = " \n %abc \r\n arg1 \n\t arg2 \r\n \t %def arg3 arg4"; 178 | commandType = inputParser.GetNextCommand(input, out symbol, out commandInput, out remainingInput); 179 | 180 | Assert.AreEqual(commandType, InputParser.CommandType.Magic); 181 | Assert.IsNotNull(symbol, $"Input:\n{input}"); 182 | Assert.IsTrue(commandInput != null && commandInput.TrimStart().StartsWith("%abc"), $"Input:\n{input}"); 183 | Assert.IsTrue(remainingInput != null && remainingInput.TrimStart().StartsWith("%def"), $"Input:\n{input}"); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tests/core/Properties/267DevDivSNKey2048.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/jupyter-core/15a08a57215b0683b59bb04298c58a4f2bbb6aa0/tests/core/Properties/267DevDivSNKey2048.snk -------------------------------------------------------------------------------- /tests/core/core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | true 7 | Microsoft.Jupyter.Core.Tests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/protocol/test_iecho.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import jupyter_kernel_test 3 | 4 | class EchoKernelTests(jupyter_kernel_test.KernelTests): 5 | # Required -------------------------------------- 6 | 7 | # The name identifying an installed kernel to run the tests against 8 | kernel_name = "iecho" 9 | 10 | # language_info.name in a kernel_info_reply should match this 11 | language_name = "Echo" 12 | 13 | # Optional -------------------------------------- 14 | 15 | # Samples of code which generate a result value (ie, some text 16 | # displayed as Out[n]) 17 | code_execute_result = [ 18 | {'code': 'foo', 'result': 'foo'} 19 | ] 20 | 21 | if __name__ == '__main__': 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /tests/protocol/test_imoon.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import jupyter_kernel_test 3 | 4 | class MoonKernelTests(jupyter_kernel_test.KernelTests): 5 | # Required -------------------------------------- 6 | 7 | # The name identifying an installed kernel to run the tests against 8 | kernel_name = "imoon" 9 | 10 | # language_info.name in a kernel_info_reply should match this 11 | language_name = "Lua" 12 | 13 | # Optional -------------------------------------- 14 | 15 | code_hello_world = "print('hello, world')" 16 | 17 | # Samples of code which generate a result value (ie, some text 18 | # displayed as Out[n]) 19 | code_execute_result = [ 20 | {'code': 'return 1 + 3', 'result': '4'} 21 | ] 22 | 23 | def test_imoon_metadata_is_correct(self): 24 | """ 25 | Some clients, e.g. nteract, require that metadata on displayable data 26 | is convertable to dict[str, dict[str, Any]]; we test that this is the 27 | case here. 28 | """ 29 | self.flush_channels() 30 | reply, output_msgs = self.execute_helper("%version") 31 | self.assertEqual(output_msgs[0]['header']['msg_type'], 'display_data') 32 | self.assert_(isinstance(output_msgs[0]['content']['metadata'], dict)) 33 | for mime_type, contents in output_msgs[0]['content']['metadata'].items(): 34 | self.assert_(isinstance(mime_type, str)) 35 | self.assert_(isinstance(contents, dict)) 36 | self.assertEqual(contents, {}) 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | --------------------------------------------------------------------------------