├── .circleci └── config.yml ├── .gitignore ├── Dockerfile ├── README.md ├── __tests__ ├── __snapshots__ │ └── index.spec.ts.snap └── index.spec.ts ├── get-prebuilt.js ├── package.json ├── src ├── index.ts └── server.ts ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/node:10.15 10 | 11 | working_directory: ~/repo 12 | 13 | steps: 14 | - checkout 15 | 16 | - restore_cache: 17 | name: Restore Yarn Package Cache 18 | keys: yarn-packages-{{ checksum "yarn.lock" }} 19 | 20 | - run: 21 | name: Install Dependencies 22 | command: yarn install --frozen-lockfile 23 | 24 | - save_cache: 25 | name: Save Yarn Package Cache 26 | key: yarn-packages-{{ checksum "yarn.lock" }} 27 | paths: 28 | - ~/.cache/yarn 29 | 30 | - run: yarn test 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | 82 | # Built definitions 83 | lib/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /home/kernel-relay 4 | ADD . . 5 | 6 | CMD npm run clean 7 | CMD npm install 8 | CMD npm run build:prod 9 | 10 | EXPOSE 4000 11 | ENTRYPOINT ["node", "lib/index.js"] 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @nteract/kernel-relay 2 | 3 | This package provides a GraphQL API for managing communication between a 4 | Jupyter kernel and front-end clients. The package allows users to control and 5 | monitor kernels. 6 | 7 | Through GraphQL queries and mutations, the API enables the user to launch 8 | kernels, subscribe to the status of a kernel, send Jupyter messages from a 9 | client to a kernel, and more. 10 | 11 | The architecture diagram below shows how the kernel-relay sits alongisde Jupyter kernels 12 | and serves clients. 13 | 14 | ![nteract-kernel-relay](https://user-images.githubusercontent.com/1857993/54857237-6b0b2a80-4cbb-11e9-8ca9-68c4a25359a2.png) 15 | 16 | ## Installation 17 | 18 | ``` 19 | $ yarn add @nteract/kernel-relay 20 | ``` 21 | 22 | ``` 23 | $ npm install --save @nteract/kernel-relay 24 | ``` 25 | 26 | ## Developer Installation 27 | 28 | To get started developing on `kernel-relay`, you'll need to clone this repository. 29 | 30 | ``` 31 | $ git clone https://github.com/nteract/kernel-relay.git 32 | ``` 33 | 34 | Then, you'll need to install the dependencies within this repository. 35 | 36 | ``` 37 | $ yarn 38 | OR 39 | $ npm install 40 | ``` 41 | 42 | You can then run the GraphQL server with a playground UI. 43 | 44 | ``` 45 | $ yarn start 46 | ``` 47 | 48 | ## Docker Setup 49 | 50 | To run an instance of the kernel-relay within a Docker container, you can execute the following commands. Note that you will need to have Docker installed on your machine. 51 | 52 | First, run the following in the cloned directory to build a Docker image. 53 | 54 | ``` 55 | $ docker build -t nteract/kernel-relay-dev:latest . 56 | ``` 57 | 58 | Then, run a Docker container based on this image. 59 | 60 | ``` 61 | $ docker run -p 4000:4000 -t nteract/kernel-relay-dev:latest 62 | ``` 63 | 64 | Next, navigate to http://localhost:4000 in your browser to interact with the instance of the kernel-relay running within your Docker container. 65 | 66 | ## Usage 67 | 68 | The query example below showcases how to use the GraphQL API to get the status 69 | of a kernel. 70 | 71 | ``` 72 | > query GetKernels { 73 | listKernelSpecs { 74 | name 75 | } 76 | } 77 | 78 | { 79 | "data": { 80 | "listKernelSpecs": [ 81 | { 82 | "name": "python3" 83 | } 84 | ] 85 | } 86 | } 87 | 88 | 89 | > mutation StartJupyterKernel { 90 | startKernel(name: "python3") { 91 | id 92 | status 93 | } 94 | } 95 | 96 | { 97 | "data": { 98 | "startKernel": [ 99 | { 100 | "name": "python3", 101 | "id": "a-uuid", 102 | "status": "started" 103 | } 104 | ] 105 | } 106 | } 107 | ``` 108 | 109 | ## Documentation 110 | 111 | We're working on adding more documentation for this component. Stay tuned by 112 | watching this repository! 113 | 114 | ## Support 115 | 116 | If you experience an issue while using this package or have a feature request, 117 | please file an issue on the [issue board](https://github.com/nteract/nteract/issues/new/choose) 118 | and, if possible, add the `pkg:kernel-relay` label. 119 | 120 | ## License 121 | 122 | [BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/) 123 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Mutations launches a kernel 1`] = ` 4 | Object { 5 | "data": Object { 6 | "startKernel": Object { 7 | "id": "kernel1", 8 | "status": "launched", 9 | }, 10 | }, 11 | "errors": undefined, 12 | "extensions": undefined, 13 | "http": Object { 14 | "headers": Headers { 15 | Symbol(map): Object {}, 16 | }, 17 | }, 18 | } 19 | `; 20 | 21 | exports[`Mutations shuts down a kernel 1`] = ` 22 | Object { 23 | "data": Object { 24 | "shutdownKernel": Object { 25 | "id": "kernel1", 26 | "status": "shutdown", 27 | }, 28 | }, 29 | "errors": undefined, 30 | "extensions": undefined, 31 | "http": Object { 32 | "headers": Headers { 33 | Symbol(map): Object {}, 34 | }, 35 | }, 36 | } 37 | `; 38 | 39 | exports[`Queries returns a list of kernelspecs 1`] = ` 40 | Object { 41 | "data": Object { 42 | "listKernelSpecs": Array [ 43 | Object { 44 | "name": "python3", 45 | }, 46 | ], 47 | }, 48 | "errors": undefined, 49 | "extensions": undefined, 50 | "http": Object { 51 | "headers": Headers { 52 | Symbol(map): Object {}, 53 | }, 54 | }, 55 | } 56 | `; 57 | -------------------------------------------------------------------------------- /__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | import { createTestClient } from "apollo-server-testing"; 3 | import { createServer } from "../src/server"; 4 | 5 | jest.mock("uuid", () => ({ 6 | v4: jest.fn(() => "kernel1") 7 | })); 8 | 9 | jest.mock("@nteract/fs-kernels", () => ({ 10 | async findAll() { 11 | return { 12 | python3: { 13 | name: "python3", 14 | files: [ 15 | "/Users/jovyan/Library/Jupyter/kernels/python3/kernel.json", 16 | "/Users/jovyan/Library/Jupyter/kernels/python3/logo-32x32.png", 17 | "/Users/jovyan/Library/Jupyter/kernels/python3/logo-64x64.png" 18 | ], 19 | resource_dir: "/Users/jovyan/Library/Jupyter/kernels/python3", 20 | spec: { 21 | argv: [ 22 | "/usr/local/bin/python3", 23 | "-m", 24 | "ipykernel_launcher", 25 | "-f", 26 | "{connection_file}" 27 | ], 28 | env: {}, 29 | display_name: "Python 3", 30 | language: "python", 31 | interrupt_mode: "signal" 32 | } 33 | } 34 | }; 35 | }, 36 | async launchKernel(name: string) { 37 | return { 38 | connectionInfo: { 39 | key: "kernel1" 40 | }, 41 | shutdown: jest.fn(), 42 | channels: { 43 | next: jest.fn(), 44 | subscribe: jest.fn(), 45 | unsubscribe: jest.fn(), 46 | complete: jest.fn() 47 | } 48 | }; 49 | } 50 | })); 51 | 52 | describe("Queries", () => { 53 | it("returns a list of kernelspecs", async () => { 54 | const { query } = createTestClient(createServer()); 55 | const LIST_KERNELSPECS = gql` 56 | query GetKernels { 57 | listKernelSpecs { 58 | name 59 | } 60 | } 61 | `; 62 | const response = await query({ query: LIST_KERNELSPECS }); 63 | expect(response).toMatchSnapshot(); 64 | }); 65 | }); 66 | 67 | describe("Mutations", () => { 68 | let kernelId: string; 69 | it("launches a kernel", async () => { 70 | const { mutate } = createTestClient(createServer()); 71 | const START_KERNEL = gql` 72 | mutation StartJupyterKernel { 73 | startKernel(name: "python3") { 74 | id 75 | status 76 | } 77 | } 78 | `; 79 | 80 | const response = await mutate({ mutation: START_KERNEL }); 81 | 82 | // When the response has errors, they'll be an array 83 | // and it's easier to diagnose if we use an expect matcher here 84 | expect(response.errors).toBeUndefined(); 85 | 86 | kernelId = response.data ? response.data.startKernel.id: undefined; 87 | 88 | expect(response).toMatchSnapshot(); 89 | }); 90 | it("shuts down a kernel", async () => { 91 | const { mutate } = createTestClient(createServer()); 92 | const SHUTDOWN_KERNEL = gql` 93 | mutation KillJupyterKernel($id: ID) { 94 | shutdownKernel(id: $id) { 95 | id 96 | status 97 | } 98 | } 99 | `; 100 | const response = await mutate({ 101 | mutation: SHUTDOWN_KERNEL, 102 | variables: { id: kernelId } 103 | } as any); 104 | 105 | // When the response has errors, they'll be an array 106 | // and it's easier to diagnose if we use an expect matcher here 107 | expect(response.errors).toBeUndefined(); 108 | 109 | expect(response).toMatchSnapshot(); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /get-prebuilt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module exists entirely to assist with getting the right 3 | * version of zeromq for the target platform on postinstall. 4 | * 5 | * To ease this setup, we'll keep this as a plain js file. 6 | */ 7 | 8 | const path = require("path"); 9 | const fs = require("fs"); 10 | const mkdirp = require("mkdirp"); 11 | 12 | const placementDir = path.join("lib", "build", "Release"); 13 | mkdirp.sync(placementDir); 14 | const placement = path.join(placementDir, "zmq.node"); 15 | 16 | const zmqDir = path.dirname(require.resolve("zeromq")); 17 | const builtModule = path.join(zmqDir, "build", "Release", "zmq.node"); 18 | 19 | fs.copyFileSync(builtModule, placement); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nteract/kernel-relay", 3 | "version": "2.0.2", 4 | "description": "This package provides a GraphQL API for managing communication between a Jupyter kernel and clients. The package allows users to launch kernels, subscribe to the status of a kernel, send Jupyter messages from a client to a kernel, and more.", 5 | "bin": { 6 | "kernel-relay": "lib/index.js" 7 | }, 8 | "main": "lib/index.js", 9 | "types": "lib/index.d.ts", 10 | "scripts": { 11 | "postinstall": "npm run get-zeromq", 12 | "get-zeromq": "node get-prebuilt.js", 13 | "prepublishOnly": "npm run build:prod && rm lib/build/Release/zmq.node", 14 | "build": "ncc build src/index.ts -o lib", 15 | "build:prod": "npm run build -- -m -s", 16 | "build:watch": "npm run build -- --watch", 17 | "start": "ncc run src/index.ts", 18 | "dev": "concurrently \"nodemon lib/index.js\" \"npm run build:watch\"", 19 | "test": "npx jest", 20 | "clean": "rimraf node_modules" 21 | }, 22 | "jest": { 23 | "preset": "ts-jest", 24 | "globals": { 25 | "ts-jest": { 26 | "tsConfig": "tsconfig.json" 27 | } 28 | }, 29 | "moduleFileExtensions": [ 30 | "js", 31 | "jsx", 32 | "ts", 33 | "tsx" 34 | ] 35 | }, 36 | "keywords": [ 37 | "jupyter", 38 | "graphql", 39 | "server" 40 | ], 41 | "files": [ 42 | "lib", 43 | "get-prebuilt.js" 44 | ], 45 | "publishConfig": { 46 | "access": "public" 47 | }, 48 | "author": "nteract Contributors", 49 | "license": "BSD-3-Clause", 50 | "dependencies": { 51 | "zeromq": "^5.1.0" 52 | }, 53 | "devDependencies": { 54 | "@nteract/fs-kernels": "^2.0.1", 55 | "@nteract/messaging": "^6.0.1", 56 | "@types/graphql-type-json": "^0.1.3", 57 | "@types/jest": "^23.3.13", 58 | "@zeit/ncc": "^0.18.5", 59 | "apollo-server": "^2.3.1", 60 | "apollo-server-testing": "^2.3.1", 61 | "concurrently": "^4.1.0", 62 | "enchannel-zmq-backend": "^9.0.1", 63 | "graphql": "^14.0.2", 64 | "graphql-type-json": "^0.2.1", 65 | "jest": "^24.0.0", 66 | "mkdirp": "^0.5.1", 67 | "nodemon": "^1.18.9", 68 | "ts-jest": "^23.10.5", 69 | "typescript": "^3.3.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer, kernels } from "./server"; 2 | 3 | async function main() { 4 | // In the most basic sense, the ApolloServer can be started 5 | // by passing type definitions (typeDefs) and the resolvers 6 | // responsible for fetching the data for those types. 7 | const server = createServer(); 8 | // This `listen` method launches a web-server. Existing apps 9 | // can utilize middleware options, which we'll discuss later. 10 | server.listen(4000, "0.0.0.0").then(({ url }) => { 11 | console.log(`🚀 Server ready at ${url}`); 12 | }); 13 | } 14 | 15 | process.on("exit", () => { 16 | Object.keys(kernels).map(async id => { 17 | console.log("shutting down ", id); 18 | await kernels[id].shutdown(); 19 | }); 20 | }); 21 | 22 | main(); 23 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module kernel-relay 3 | */ 4 | import { ApolloServer, Config, gql } from "apollo-server"; 5 | import GraphQLJSON from "graphql-type-json"; 6 | 7 | import { findAll, Kernel, launchKernel } from "@nteract/fs-kernels"; 8 | import { JupyterMessage, kernelInfoRequest } from "@nteract/messaging"; 9 | 10 | const Types = gql` 11 | scalar JSON 12 | 13 | type KernelSpec { 14 | argv: [String] 15 | display_name: String 16 | files: [String] 17 | id: ID! 18 | language: String 19 | name: String 20 | resources_dir: String 21 | } 22 | 23 | type KernelSession { 24 | id: ID! 25 | status: String 26 | } 27 | 28 | type Message { 29 | id: ID! 30 | payload: JSON 31 | } 32 | `; 33 | 34 | const Query = gql` 35 | type Query { 36 | listKernelSpecs: [KernelSpec!]! 37 | running: [KernelSession!]! 38 | messages: [Message!]! 39 | } 40 | `; 41 | 42 | const Mutation = gql` 43 | type Mutation { 44 | startKernel(name: String): KernelSession! 45 | shutdownKernel(id: ID): KernelSession! 46 | } 47 | `; 48 | 49 | interface StartKernel { 50 | name: string; 51 | } 52 | 53 | interface ShutdownKernel { 54 | id: string; 55 | } 56 | 57 | export const kernels: { [id: string]: Kernel } = {}; 58 | 59 | const messages: { 60 | [kernelId: string]: JupyterMessage[]; 61 | } = {}; 62 | 63 | const typeDefs = [Types, Query, Mutation]; 64 | const resolvers = { 65 | JSON: GraphQLJSON, 66 | Mutation: { 67 | startKernel: async (parentValue: any, args: StartKernel) => { 68 | const kernel = await launchKernel(args.name); 69 | 70 | console.log(`kernel ${args.name}:${kernel.connectionInfo.key} launched`); 71 | 72 | // NOTE: we should generate IDs 73 | // We're also setting a session ID within the enchannel-zmq setup, I wonder 74 | // if we should use that 75 | const id = kernel.connectionInfo.key; 76 | 77 | messages[id] = []; 78 | kernels[id] = kernel; 79 | 80 | const subscription = kernel.channels.subscribe( 81 | (message: JupyterMessage) => { 82 | messages[id].push(message); 83 | } 84 | ); 85 | 86 | const request = kernelInfoRequest(); 87 | messages[id].push(request); 88 | kernel.channels.next(request); 89 | 90 | // NOTE: We are going to want to both: 91 | // 92 | // subscription.unsubscribe() 93 | // AND 94 | // kernel.channels.complete() 95 | // 96 | // Within our cleanup code 97 | 98 | return { 99 | id, 100 | status: "launched" 101 | }; 102 | }, 103 | shutdownKernel: async (parentValue: any, args: ShutdownKernel) => { 104 | const { id } = args; 105 | const kernel = kernels[id]; 106 | 107 | await kernel.shutdown(); 108 | kernel.channels.unsubscribe(); 109 | kernel.channels.complete(); 110 | 111 | return { 112 | id, 113 | status: "shutdown" 114 | }; 115 | } 116 | }, 117 | Query: { 118 | listKernelSpecs: async () => { 119 | const kernelspecs = await findAll(); 120 | 121 | return Object.keys(kernelspecs).map(key => { 122 | const { files, name, resources_dir, spec } = kernelspecs[key]; 123 | return { 124 | id: key, 125 | name, 126 | files, 127 | resources_dir, 128 | ...spec 129 | }; 130 | }); 131 | }, 132 | messages: () => { 133 | return ([] as JupyterMessage[]) 134 | .concat(...Object.values(messages)) 135 | .map(message => ({ id: message.header.msg_id, payload: message })); 136 | }, 137 | running: () => { 138 | return Object.keys(kernels).map(id => ({ id, status: "pretend" })); 139 | } 140 | } 141 | }; 142 | 143 | export function createServer(options?: Config): ApolloServer { 144 | return new ApolloServer({ 145 | introspection: true, 146 | // Since we're playing around, enable features for introspection and playing on our current deployment 147 | // If this gets used in a "real" production capacity, introspection and playground should be disabled 148 | // based on NODE_ENV === "production" 149 | playground: { 150 | /*tabs: [ 151 | { 152 | endpoint: "", 153 | query: `` 154 | } 155 | ]*/ 156 | }, 157 | resolvers: resolvers as any, 158 | typeDefs, 159 | ...options 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "composite": true, 7 | "lib": ["esnext", "dom"], 8 | "target": "es2015", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "skipLibCheck": true, 12 | "noImplicitAny": true, 13 | "plugins": [ 14 | { "name": "typescript-styled-plugin" }, 15 | { "name": "typescript-tslint-plugin" } 16 | ], 17 | "resolveJsonModule": true, 18 | "preserveWatchOutput": true, 19 | "jsx": "react", 20 | "typeRoots": ["./node_modules/@types", "types"], 21 | "outDir": "lib", 22 | "rootDir": "src" 23 | }, 24 | "include": ["src"] 25 | } 26 | --------------------------------------------------------------------------------