Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quantum-kittens
GitHub Repository: quantum-kittens/platypus
Path: blob/main/frontend/vue/components/CodeExercise/KernelManager.ts
3375 views
1
/* eslint-disable no-console */
2
import { KernelManager, KernelAPI, ServerConnection } from '@jupyterlab/services'
3
import { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel'
4
import { IStreamMsg } from '@jupyterlab/services/lib/kernel/messages'
5
import { WidgetsManager } from './WidgetsManager'
6
7
export { IKernelConnection, IStreamMsg }
8
9
export interface ISavedSession {
10
enabled: boolean,
11
maxAge: number,
12
storagePrefix: string
13
}
14
15
export interface IBinderOptions {
16
repo: string,
17
ref: string,
18
binderUrl: string,
19
savedSession: ISavedSession
20
}
21
22
export interface IServerSettings {
23
appendToken: boolean
24
}
25
26
export interface IKernelOptions {
27
name: string,
28
kernelName: string,
29
path: string,
30
serverSettings: IServerSettings
31
}
32
33
export interface IServerOptions {
34
binderOptions: IBinderOptions,
35
kernelOptions: IKernelOptions
36
}
37
38
const targetEnvironment = window.location.hostname.includes('localhost') ? 'staging' : 'production'
39
40
export const serverOptions: IServerOptions = {
41
binderOptions: {
42
repo: 'Qiskit/platypus-binder',
43
ref: targetEnvironment,
44
binderUrl: 'https://mybinder.org',
45
savedSession: {
46
enabled: true,
47
maxAge: 86400,
48
storagePrefix: `qiskit-binder-${targetEnvironment}`
49
}
50
},
51
kernelOptions: {
52
name: 'python3',
53
kernelName: 'python3',
54
path: '.',
55
serverSettings: {
56
appendToken: true
57
}
58
}
59
}
60
61
export const events = new EventTarget()
62
let requestKernelPromise: Promise<IKernelConnection>
63
let _widgetsManager: WidgetsManager | undefined
64
65
export function getWidgetsManager () { return _widgetsManager }
66
67
export function requestBinderKernel () {
68
if (requestKernelPromise === undefined) {
69
requestKernelPromise = requestBinder().then((serverSettings: ServerConnection.ISettings) => {
70
serverOptions.kernelOptions.serverSettings = serverSettings
71
serverOptions.kernelOptions.serverSettings.appendToken = true
72
return requestKernel().then((kernel) => {
73
_widgetsManager = new WidgetsManager(kernel)
74
return kernel
75
})
76
})
77
}
78
79
// request a Kernel from Binder
80
// this strings together requestBinder and requestKernel.
81
// returns a Promise for a running Kernel.
82
return requestKernelPromise
83
}
84
85
function buildBinderUrlToRepo (repo: string, binderUrl: string, ref: string) {
86
// trim github.com from repo
87
let cleanRepo = repo.replace(/^(https?:\/\/)?github.com\//, '')
88
// trim trailing or leading '/' on repo
89
cleanRepo = cleanRepo.replace(/(^\/)|(\/?$)/g, '')
90
// trailing / on binderUrl
91
const cleanBinderUrl = binderUrl.replace(/(\/?$)/g, '')
92
93
return cleanBinderUrl + '/build/gh/' + cleanRepo + '/' + ref
94
}
95
96
function makeSettings (msg: any): ServerConnection.ISettings {
97
return ServerConnection.makeSettings({
98
baseUrl: msg.url,
99
wsUrl: 'ws' + msg.url.slice(4),
100
token: msg.token,
101
appendToken: true
102
})
103
}
104
105
function isExpiredSession (existingServer: any, maxAge: number) {
106
const lastUsed = existingServer.lastUsed
107
const now = new Date().getTime()
108
const ageSeconds = (now - lastUsed) / 1000
109
110
return ageSeconds > maxAge
111
}
112
113
function getStoredServer (storageKey: string, maxAge: number) {
114
const storedInfoJSON = window.localStorage.getItem(storageKey)
115
if (storedInfoJSON === null) {
116
console.debug('No session saved in ', storageKey)
117
return
118
}
119
console.debug('Saved binder session detected')
120
121
let existingServer
122
try {
123
existingServer = JSON.parse(storedInfoJSON)
124
} catch {
125
console.debug('Bad JSON format')
126
return
127
}
128
129
if (isExpiredSession(existingServer, maxAge)) {
130
console.debug(`Not using expired binder session for ${existingServer.url} from ${existingServer.lastUsed}`)
131
window.localStorage.removeItem(storageKey)
132
return
133
}
134
135
return existingServer
136
}
137
138
async function getExistingServer (savedSession: ISavedSession, storageKey: string) {
139
if (!savedSession.enabled) {
140
return
141
}
142
143
const existingServer = getStoredServer(storageKey, savedSession.maxAge)
144
if (!existingServer) {
145
return
146
}
147
148
const settings = ServerConnection.makeSettings({
149
baseUrl: existingServer.url,
150
wsUrl: 'ws' + existingServer.url.slice(4),
151
token: existingServer.token,
152
appendToken: true
153
})
154
155
try {
156
await KernelAPI.listRunning(settings)
157
} catch (err) {
158
console.debug('Saved binder connection appears to be invalid, requesting new session', err)
159
window.localStorage.removeItem(storageKey)
160
return
161
}
162
163
existingServer.lastUsed = new Date().getTime()
164
try {
165
window.localStorage.setItem(storageKey, JSON.stringify(existingServer))
166
} catch (err) {
167
console.warn('localStorage.setItem failed, not saving session locally', err)
168
}
169
170
return settings
171
}
172
173
function logBuildingStatus () {
174
const detail = {
175
status: 'binder-building',
176
message: 'Requesting build from binder'
177
}
178
179
events.dispatchEvent(new CustomEvent('status', { detail }))
180
181
console.debug('status')
182
console.debug(detail)
183
}
184
185
function logLostConnection (url: string, err: Event) {
186
const errorMessage = `Lost connection to Binder: ${url}`
187
const detail = {
188
status: 'binder-failed',
189
message: errorMessage,
190
error: err
191
}
192
console.error(errorMessage, err)
193
events.dispatchEvent(new CustomEvent('status', { detail }))
194
}
195
196
function logBinderPhase (status: string, binderMessage: string) {
197
const message = `Phase: Binder is ${status}`
198
console.debug(message)
199
const detail = {
200
status: `binder-${status}`,
201
message,
202
binderMessage
203
}
204
events.dispatchEvent(new CustomEvent('status', { detail }))
205
}
206
207
function logBinderMessage (message: string) {
208
if (message) {
209
console.debug(`Binder: ${message}`)
210
}
211
212
events.dispatchEvent(new CustomEvent('message', { detail: { message } }))
213
}
214
215
async function executeInitCode () {
216
const loc = window.location.hostname + window.location.pathname
217
const code = `%set_env QE_CUSTOM_CLIENT_APP_HEADER=${loc}`
218
try {
219
const kernel = await requestBinderKernel()
220
const requestFuture = kernel.requestExecute({ code })
221
requestFuture.done.then(() => {
222
logBinderMessage('init code executed')
223
})
224
} catch (error: any) {
225
logBinderMessage(`failed to execute init code: ${error}`)
226
}
227
}
228
229
export async function requestBinder () {
230
const binderUrl = serverOptions.binderOptions.binderUrl
231
const ref = serverOptions.binderOptions.ref
232
const repo = serverOptions.binderOptions.repo
233
const savedSession = serverOptions.binderOptions.savedSession
234
const url = buildBinderUrlToRepo(repo, binderUrl, ref)
235
const storageKey = savedSession.storagePrefix + url
236
console.debug('binder url', binderUrl)
237
console.debug('binder ref', ref)
238
console.debug('binder repo', repo)
239
console.debug('binder savedSession', savedSession)
240
console.debug('binder build URL', url)
241
console.debug('binder storageKey', storageKey)
242
243
const existingServer = await getExistingServer(savedSession, storageKey)
244
if (existingServer) {
245
executeInitCode()
246
return existingServer
247
}
248
249
logBuildingStatus()
250
251
return new Promise<ServerConnection.ISettings>((resolve, reject) => {
252
const es = new EventSource(url)
253
es.onerror = (err) => {
254
es.close()
255
logLostConnection(url, err)
256
reject(new Error('Lost connection to the server'))
257
}
258
259
let lastPhase: string = ''
260
es.onmessage = (evt) => {
261
const msg = JSON.parse(evt.data)
262
263
if (msg.phase && msg.phase !== lastPhase) {
264
lastPhase = msg.phase
265
logBinderPhase(msg.phase, msg.message || '')
266
}
267
268
logBinderMessage(msg.message)
269
270
switch (msg.phase) {
271
case 'failed':
272
console.error('Failed to build', url, msg)
273
es.close()
274
reject(new Error(msg))
275
break
276
case 'ready':
277
executeInitCode()
278
console.debug('Binder ready, storing server and resolve', msg)
279
es.close()
280
storeServer(storageKey, msg)
281
resolve(makeSettings(msg))
282
break
283
default:
284
}
285
}
286
})
287
}
288
289
function storeServer (key: string, msg: {url: string, token: string}) {
290
try {
291
window.localStorage.setItem(
292
key,
293
JSON.stringify({
294
url: msg.url,
295
token: msg.token,
296
lastUsed: new Date()
297
})
298
)
299
} catch (e) {
300
// storage quota full, gently ignore nonfatal error
301
console.warn("Couldn't save thebe binder connection info to local storage", e)
302
}
303
}
304
305
// requesting Kernels
306
export function requestKernel () {
307
// request a new Kernel
308
const kernelOptions = serverOptions.kernelOptions
309
const serverSettings = ServerConnection.makeSettings(kernelOptions.serverSettings)
310
311
events.dispatchEvent(new CustomEvent('status', {
312
detail: {
313
status: 'starting',
314
message: 'Starting Kernel'
315
}
316
}))
317
318
const kernelManager = new KernelManager({ serverSettings })
319
return kernelManager.ready
320
.then(() => kernelManager.startNew(kernelOptions))
321
.then((kernel: IKernelConnection) => {
322
events.dispatchEvent(new CustomEvent('status', {
323
detail: {
324
status: 'ready',
325
message: 'Kernel is ready',
326
kernel
327
}
328
}))
329
return kernel
330
})
331
}
332
333