import log4javascript from 'log4javascript'
import debounce from 'lodash.debounce'
import { Kernel, ServerConnection } from '@jupyterlab/services'
import { WidgetManager } from './manager'
import * as util from './util.js'
import BinderHub from './BinderHub'
const DEFAULT_BASE_URL = 'https://mybinder.org'
const DEFAULT_PROVIDER = 'gh'
const DEFAULT_SPEC = 'SamLau95/nbinteract-image/master'
var message = {};
/**
* Main entry point for nbinteract
*
* Class that runs notebook code and create widgets
*/
export default class NbInteract {
/**
* Initialize NbInteract. Does not start kernel until run() is called.
*
* @param {Object} [config] - Configuration for NbInteract
*
* @param {String} [config.spec] - BinderHub spec for Jupyter image. Must be
* in the format: `${username}/${repo}/${branch}`. Defaults to
* 'SamLau95/nbinteract-image/master'.
*
* @param {String} [config.baseUrl] - Binder URL to start server. Defaults to
* https://mybinder.org.
*
* @param {String} [config.provider] - BinderHub provider. Defaults to 'gh'
* for GitHub.
*
* @param {String} [config.nbUrl] - Full URL of a running notebook server.
* If set, NbInteract ignores all Binder config and will directly request
* Python kernels from the notebook server.
*
* Defaults to `false` by default as Binder is used to start
* a notebook server.
*/
constructor({
spec = DEFAULT_SPEC,
baseUrl = DEFAULT_BASE_URL,
provider = DEFAULT_PROVIDER,
nbUrl = false,
logger = false,
j1API = false,
} = {}) {
this.logger = logger;
this.j1 = j1API;
this.run = debounce(this.run, 500, {
leading: true,
trailing: false,
})
this._kernelHeartbeat = this._kernelHeartbeat.bind(this)
this.binder = new BinderHub({ spec, baseUrl, provider, nbUrl, logger, j1API })
// Keep track of properties for debugging
this.kernel = null
this.manager = null
}
/**
* Attaches event listeners to page that call run() when clicked. Updates
* status text of elements as server is started until widget is rendered.
* When widgets are rendered, removes all status elements.
*
* If a running kernel is cached in localStorage, creates widgets without
* needing button click.
*/
async prepare() {
this.logger.info('\n' + 'nbinteract.prepare: ' + 'nbinteract core is being initialized');
// The widget buttons show loading indicator text by default. At this
// point, nbinteract is ready to run so we change the button text to match.
util.setButtonsStatus('Show widgets')
this.binder.registerCallback('failed', (oldState, newState, data) => {
message.type = 'command';
message.action = 'error';
message.text = `Error, try refreshing the page:
${data.message}`;
this.j1.sendMessage('nbinteract-core.prepare', 'j1.adapter.nbinteract', message);
// util.setButtonsError(
// `Error, try refreshing the page:
${data.message}`,
// )
})
// util.statusButtons().forEach(button => {
// button.addEventListener('click', e => {
// this.run()
// })
// })
this.runIfKernelExists()
}
/**
* Starts kernel if needed, runs code on page, and initializes widgets.
*/
async run() {
// The logic to remove the status buttons is temporarily in
// manager.js:_displayWidget since it's tricky to implement here.
// TODO(sam): Move the logic here instead.
util.setButtonsStatus('Initializing widgets ...')
message.type = 'command';
message.action = 'info';
message.text = 'Initializing widgets ...';
this.j1.sendMessage('nbinteract-core.run', 'j1.adapter.nbinteract', message);
// Normally, we wait until one widget displays before removing the show
// widget buttons. However, if there are no widgets on the page, we should
// just remove all buttons since the top level button is generated
// regardless of whether the page contains widgets.
if (util.codeCells().length === 0) {
util.removeButtons()
}
const firstRun = !this.kernel || !this.manager
try {
this.kernel = await this._getOrStartKernel()
this.manager = this.manager || new WidgetManager(this.kernel)
this.manager.generateWidgets()
message.type = 'command';
message.action = 'info';
message.text = 'Widget initialization finished.';
this.j1.sendMessage('nbinteract-core.run', 'j1.adapter.nbinteract', message);
message.type = 'command';
message.action = 'nbi_init_finished';
message.text = 'nbinteract initialization finished.';
this.j1.sendMessage('nbinteract-core.run', 'j1.adapter.nbinteract', message);
if (firstRun) this._kernelHeartbeat()
} catch (err) {
debugger
// console.log ('Error in widget initialization :(')
this.logger.info('\n' + 'nbinteract-core.run: '+ 'widget initialization failed');
message.type = 'command';
message.action = 'error';
message.text = 'Widget initialization failed.';
this.j1.sendMessage('nbinteract-core.run', 'j1.adapter.nbinteract', message);
// throw err
}
}
/**
* Same as run(), but only runs code if kernel is already started.
*/
async runIfKernelExists() {
try {
await this._getKernelModel()
} catch (err) {
// console.log (
// 'No kernel, stopping the runIfKernelExists() call. Use the',
// 'run() method to automatically start a kernel if needed.',
// )
this.logger.info('\n' + 'nbinteract.runIfKernelExists: '
+ 'no kernel, stopping the runIfKernelExists() call.'
+ '\n'
+ 'use the run() method to automatically start a kernel if needed'
);
// jadams
// call 'run()' method to automatically start a kernel
this.run()
return
}
this.run()
}
/**********************************************************************
* Private methods
**********************************************************************/
/**
* Checks kernel connection every seconds_between_check seconds. If the
* kernel is dead, starts a new kernel and re-creates widgets.
*/
async _kernelHeartbeat(seconds_between_check = 5) {
try {
await this._getKernelModel()
} catch (err) {
this.logger.info('\n' + 'nbinteract.kernelHeartbeat: ' + 'looks like the kernel has died');
// console.log ('Looks like the kernel died:', err.toString())
this.logger.info('\n' + 'nbinteract.kernelHeartbeat: ' + 'starting a new kernel ...');
// console.log ('Starting a new kernel...')
message.type = 'command';
message.action = 'info';
message.text = 'Seems the kernel has died, starting a new kernel ...';
this.j1.sendMessage('nbinteract-core.kernelHeartbeat', 'j1.adapter.nbinteract', message);
const kernel = await this._startKernel()
this.kernel = kernel
this.manager.setKernel(kernel)
this.manager.generateWidgets()
} finally {
setTimeout(this._kernelHeartbeat, seconds_between_check * 5000)
}
}
/**
* Private method that starts a Binder server, then starts a kernel and
* returns the kernel information.
*
* Once initialized, this function caches the server and kernel info in
* localStorage. Future calls will attempt to use the cached info, falling
* back to starting a new server and kernel.
*/
async _getOrStartKernel() {
if (this.kernel) {
return this.kernel
}
try {
const kernel = await this._getKernel();
const kernelID = kernel._id;
// console.log ('Connected to cached kernel.')
this.logger.info('\n' + 'nbinteract.getOrStartKernel: ' + 'connected to cached kernel: ' + kernelID);
message.type = 'command';
message.action = 'info';
message.text = 'Connected to cached kernel ...';
this.j1.sendMessage('nbinteract-core.getOrStartKernel', 'j1.adapter.nbinteract', message);
return kernel
} catch (err) {
var errMsg = err.toString();
// this.logger.info('\n' + 'no cached kernel found, starting kernel on BinderHub: ' + err.toString());
this.logger.info('\n' + 'nbinteract.getOrStartKernel: ' + 'no cached kernel found.');
this.logger.info('\n' + 'nbinteract.getOrStartKernel: ' + 'starting new kernel on BinderHub ...');
message.type = 'command';
message.action = 'info';
message.text = 'No cached kernel found.';
this.j1.sendMessage('nbinteract-core.getOrStartKernel', 'j1.adapter.nbinteract', message);
message.type = 'command';
message.action = 'info';
message.text = 'Starting new kernel on BinderHub ...';
this.j1.sendMessage('nbinteract-core.getOrStartKernel', 'j1.adapter.nbinteract', message);
// console.log (
// 'No cached kernel, starting kernel on BinderHub:',
// err.toString(),
// )
const kernel = await this._startKernel()
return kernel
}
}
/**
* Connects to kernel using cached info from localStorage. Throws exception
* if kernel connection fails for any reason.
*/
async _getKernel() {
const { serverSettings, kernelModel } = await this._getKernelModel()
return await Kernel.connectTo(kernelModel, serverSettings)
}
/**
* Retrieves kernel model using cached info from localStorage. Throws
* exception if kernel doesn't exist.
*/
async _getKernelModel() {
const { serverParams, kernelId } = localStorage
const { url, token } = JSON.parse(serverParams)
const serverSettings = ServerConnection.makeSettings({
baseUrl: url,
wsUrl: util.baseToWsUrl(url),
token: token,
})
const kernelModel = await Kernel.findById(kernelId, serverSettings)
return { serverSettings, kernelModel }
}
/**
* Starts a new kernel using Binder and returns the connected kernel. Stores
* localStorage.serverParams and localStorage.kernelId .
*/
async _startKernel() {
try {
const { url, token } = await this.binder.startServer()
// Connect to the notebook webserver.
const serverSettings = ServerConnection.makeSettings({
baseUrl: url,
wsUrl: util.baseToWsUrl(url),
token: token,
})
// Start a kernel
const kernelSpecs = await Kernel.getSpecs(serverSettings)
const kernel = await Kernel.startNew({
name: kernelSpecs.default,
serverSettings,
})
// Store the params in localStorage for later use
localStorage.serverParams = JSON.stringify({ url, token })
localStorage.kernelId = kernel.id
// console.log ('Started kernel:', kernel.id)
this.logger.info('\n' + 'nbinteract-core.startKernel: ' + 'started kernel: '+ kernel.id);
message.type = 'command';
message.action = 'info';
message.text = 'Kernel successfully started.';
this.j1.sendMessage('nbinteract-core.startKernel', 'j1.adapter.nbinteract', message);
return kernel
} catch (err) {
debugger
this.logger.error('\n' + 'nbinteract-core: startKernel, ' + 'initialization kernel failed');
message.type = 'command';
message.action = 'error';
message.text = 'Initialization of the kernel failed.';
this.j1.sendMessage('nbinteract-core.startKernel', 'j1.adapter.nbinteract', message);
// console.error('Error in kernel initialization :(')
// throw err
}
}
async _killKernel() {
const kernel = await this._getKernel()
return kernel.shutdown()
}
}