Path: blob/main/frontend/vue/components/CodeExercise/KernelManager.ts
3375 views
/* eslint-disable no-console */1import { KernelManager, KernelAPI, ServerConnection } from '@jupyterlab/services'2import { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel'3import { IStreamMsg } from '@jupyterlab/services/lib/kernel/messages'4import { WidgetsManager } from './WidgetsManager'56export { IKernelConnection, IStreamMsg }78export interface ISavedSession {9enabled: boolean,10maxAge: number,11storagePrefix: string12}1314export interface IBinderOptions {15repo: string,16ref: string,17binderUrl: string,18savedSession: ISavedSession19}2021export interface IServerSettings {22appendToken: boolean23}2425export interface IKernelOptions {26name: string,27kernelName: string,28path: string,29serverSettings: IServerSettings30}3132export interface IServerOptions {33binderOptions: IBinderOptions,34kernelOptions: IKernelOptions35}3637const targetEnvironment = window.location.hostname.includes('localhost') ? 'staging' : 'production'3839export const serverOptions: IServerOptions = {40binderOptions: {41repo: 'Qiskit/platypus-binder',42ref: targetEnvironment,43binderUrl: 'https://mybinder.org',44savedSession: {45enabled: true,46maxAge: 86400,47storagePrefix: `qiskit-binder-${targetEnvironment}`48}49},50kernelOptions: {51name: 'python3',52kernelName: 'python3',53path: '.',54serverSettings: {55appendToken: true56}57}58}5960export const events = new EventTarget()61let requestKernelPromise: Promise<IKernelConnection>62let _widgetsManager: WidgetsManager | undefined6364export function getWidgetsManager () { return _widgetsManager }6566export function requestBinderKernel () {67if (requestKernelPromise === undefined) {68requestKernelPromise = requestBinder().then((serverSettings: ServerConnection.ISettings) => {69serverOptions.kernelOptions.serverSettings = serverSettings70serverOptions.kernelOptions.serverSettings.appendToken = true71return requestKernel().then((kernel) => {72_widgetsManager = new WidgetsManager(kernel)73return kernel74})75})76}7778// request a Kernel from Binder79// this strings together requestBinder and requestKernel.80// returns a Promise for a running Kernel.81return requestKernelPromise82}8384function buildBinderUrlToRepo (repo: string, binderUrl: string, ref: string) {85// trim github.com from repo86let cleanRepo = repo.replace(/^(https?:\/\/)?github.com\//, '')87// trim trailing or leading '/' on repo88cleanRepo = cleanRepo.replace(/(^\/)|(\/?$)/g, '')89// trailing / on binderUrl90const cleanBinderUrl = binderUrl.replace(/(\/?$)/g, '')9192return cleanBinderUrl + '/build/gh/' + cleanRepo + '/' + ref93}9495function makeSettings (msg: any): ServerConnection.ISettings {96return ServerConnection.makeSettings({97baseUrl: msg.url,98wsUrl: 'ws' + msg.url.slice(4),99token: msg.token,100appendToken: true101})102}103104function isExpiredSession (existingServer: any, maxAge: number) {105const lastUsed = existingServer.lastUsed106const now = new Date().getTime()107const ageSeconds = (now - lastUsed) / 1000108109return ageSeconds > maxAge110}111112function getStoredServer (storageKey: string, maxAge: number) {113const storedInfoJSON = window.localStorage.getItem(storageKey)114if (storedInfoJSON === null) {115console.debug('No session saved in ', storageKey)116return117}118console.debug('Saved binder session detected')119120let existingServer121try {122existingServer = JSON.parse(storedInfoJSON)123} catch {124console.debug('Bad JSON format')125return126}127128if (isExpiredSession(existingServer, maxAge)) {129console.debug(`Not using expired binder session for ${existingServer.url} from ${existingServer.lastUsed}`)130window.localStorage.removeItem(storageKey)131return132}133134return existingServer135}136137async function getExistingServer (savedSession: ISavedSession, storageKey: string) {138if (!savedSession.enabled) {139return140}141142const existingServer = getStoredServer(storageKey, savedSession.maxAge)143if (!existingServer) {144return145}146147const settings = ServerConnection.makeSettings({148baseUrl: existingServer.url,149wsUrl: 'ws' + existingServer.url.slice(4),150token: existingServer.token,151appendToken: true152})153154try {155await KernelAPI.listRunning(settings)156} catch (err) {157console.debug('Saved binder connection appears to be invalid, requesting new session', err)158window.localStorage.removeItem(storageKey)159return160}161162existingServer.lastUsed = new Date().getTime()163try {164window.localStorage.setItem(storageKey, JSON.stringify(existingServer))165} catch (err) {166console.warn('localStorage.setItem failed, not saving session locally', err)167}168169return settings170}171172function logBuildingStatus () {173const detail = {174status: 'binder-building',175message: 'Requesting build from binder'176}177178events.dispatchEvent(new CustomEvent('status', { detail }))179180console.debug('status')181console.debug(detail)182}183184function logLostConnection (url: string, err: Event) {185const errorMessage = `Lost connection to Binder: ${url}`186const detail = {187status: 'binder-failed',188message: errorMessage,189error: err190}191console.error(errorMessage, err)192events.dispatchEvent(new CustomEvent('status', { detail }))193}194195function logBinderPhase (status: string, binderMessage: string) {196const message = `Phase: Binder is ${status}`197console.debug(message)198const detail = {199status: `binder-${status}`,200message,201binderMessage202}203events.dispatchEvent(new CustomEvent('status', { detail }))204}205206function logBinderMessage (message: string) {207if (message) {208console.debug(`Binder: ${message}`)209}210211events.dispatchEvent(new CustomEvent('message', { detail: { message } }))212}213214async function executeInitCode () {215const loc = window.location.hostname + window.location.pathname216const code = `%set_env QE_CUSTOM_CLIENT_APP_HEADER=${loc}`217try {218const kernel = await requestBinderKernel()219const requestFuture = kernel.requestExecute({ code })220requestFuture.done.then(() => {221logBinderMessage('init code executed')222})223} catch (error: any) {224logBinderMessage(`failed to execute init code: ${error}`)225}226}227228export async function requestBinder () {229const binderUrl = serverOptions.binderOptions.binderUrl230const ref = serverOptions.binderOptions.ref231const repo = serverOptions.binderOptions.repo232const savedSession = serverOptions.binderOptions.savedSession233const url = buildBinderUrlToRepo(repo, binderUrl, ref)234const storageKey = savedSession.storagePrefix + url235console.debug('binder url', binderUrl)236console.debug('binder ref', ref)237console.debug('binder repo', repo)238console.debug('binder savedSession', savedSession)239console.debug('binder build URL', url)240console.debug('binder storageKey', storageKey)241242const existingServer = await getExistingServer(savedSession, storageKey)243if (existingServer) {244executeInitCode()245return existingServer246}247248logBuildingStatus()249250return new Promise<ServerConnection.ISettings>((resolve, reject) => {251const es = new EventSource(url)252es.onerror = (err) => {253es.close()254logLostConnection(url, err)255reject(new Error('Lost connection to the server'))256}257258let lastPhase: string = ''259es.onmessage = (evt) => {260const msg = JSON.parse(evt.data)261262if (msg.phase && msg.phase !== lastPhase) {263lastPhase = msg.phase264logBinderPhase(msg.phase, msg.message || '')265}266267logBinderMessage(msg.message)268269switch (msg.phase) {270case 'failed':271console.error('Failed to build', url, msg)272es.close()273reject(new Error(msg))274break275case 'ready':276executeInitCode()277console.debug('Binder ready, storing server and resolve', msg)278es.close()279storeServer(storageKey, msg)280resolve(makeSettings(msg))281break282default:283}284}285})286}287288function storeServer (key: string, msg: {url: string, token: string}) {289try {290window.localStorage.setItem(291key,292JSON.stringify({293url: msg.url,294token: msg.token,295lastUsed: new Date()296})297)298} catch (e) {299// storage quota full, gently ignore nonfatal error300console.warn("Couldn't save thebe binder connection info to local storage", e)301}302}303304// requesting Kernels305export function requestKernel () {306// request a new Kernel307const kernelOptions = serverOptions.kernelOptions308const serverSettings = ServerConnection.makeSettings(kernelOptions.serverSettings)309310events.dispatchEvent(new CustomEvent('status', {311detail: {312status: 'starting',313message: 'Starting Kernel'314}315}))316317const kernelManager = new KernelManager({ serverSettings })318return kernelManager.ready319.then(() => kernelManager.startNew(kernelOptions))320.then((kernel: IKernelConnection) => {321events.dispatchEvent(new CustomEvent('status', {322detail: {323status: 'ready',324message: 'Kernel is ready',325kernel326}327}))328return kernel329})330}331332333