diff --git a/examples/sessions.ipynb b/examples/sessions.ipynb new file mode 100644 index 00000000..7f290789 --- /dev/null +++ b/examples/sessions.ipynb @@ -0,0 +1,131 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Session Manager" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipylab import JupyterFrontEnd\n", + "app = JupyterFrontEnd()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## show all sessions from the global `SessionManager` instance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "app.sessions.running()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Show current session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "app.sessions.current_session" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example: Create Console with current session\n", + "The following two commands should both create a console panel sharing the same `session` as the current notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "app.commands.execute(\n", + " 'console:create', \n", + " app.sessions.current_session)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "app.commands.execute(\n", + " 'notebook:create-console', \n", + " {})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Force update session (asynchronous)\n", + "sessions should be updated automatically, you can force a call to `SessionManager.refreshRunning()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "from ipywidgets import Output\n", + "\n", + "out = Output()\n", + "async def refresh_running():\n", + " await app.sessions.refresh_running()\n", + " sesssions = app.sessions.running()\n", + " out.append_stdout('Session Refreshed')\n", + "\n", + "asyncio.create_task(refresh_running())\n", + "out" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/ipylab/jupyterfrontend.py b/ipylab/jupyterfrontend.py index e98c6eaa..8773dd09 100644 --- a/ipylab/jupyterfrontend.py +++ b/ipylab/jupyterfrontend.py @@ -12,6 +12,7 @@ from .commands import CommandRegistry from .shell import Shell +from .sessions import SessionManager @register @@ -23,9 +24,16 @@ class JupyterFrontEnd(Widget): version = Unicode(read_only=True).tag(sync=True) shell = Instance(Shell).tag(sync=True, **widget_serialization) commands = Instance(CommandRegistry).tag(sync=True, **widget_serialization) + sessions = Instance(SessionManager).tag(sync=True, **widget_serialization) def __init__(self, *args, **kwargs): - super().__init__(*args, shell=Shell(), commands=CommandRegistry(), **kwargs) + super().__init__( + *args, + shell=Shell(), + commands=CommandRegistry(), + sessions=SessionManager(), + **kwargs + ) self._ready_event = asyncio.Event() self._on_ready_callbacks = CallbackDispatcher() self.on_msg(self._on_frontend_msg) diff --git a/ipylab/sessions.py b/ipylab/sessions.py new file mode 100644 index 00000000..f34b6b67 --- /dev/null +++ b/ipylab/sessions.py @@ -0,0 +1,47 @@ +"""Expose current and all sessions to python kernel +""" + +import asyncio + +from ipywidgets import CallbackDispatcher, Widget, register, widget_serialization +from traitlets import List, Unicode, Dict + +from ._frontend import module_name, module_version + + +@register +class SessionManager(Widget): + """Expose JupyterFrontEnd.serviceManager.sessions""" + + _model_name = Unicode("SessionManagerModel").tag(sync=True) + _model_module = Unicode(module_name).tag(sync=True) + _model_module_version = Unicode(module_version).tag(sync=True) + + # information of the current session + current_session = Dict(read_only=True).tag(sync=True) + # keeps track of the list of sessions + sessions = List([], read_only=True).tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._refreshed_event = None + self._on_refresh_callbacks = CallbackDispatcher() + self.on_msg(self._on_frontend_msg) + + def _on_frontend_msg(self, _, content, buffers): + if content.get("event", "") == "sessions_refreshed": + self._refreshed_event.set() + self._on_refresh_callbacks() + + async def refresh_running(self): + """Force a call to refresh running sessions + + Resolved when `SessionManager.runnigSession` resolves + """ + self.send({"func": "refreshRunning"}) + self._refreshed_event = asyncio.Event() + await self._refreshed_event.wait() + + def running(self): + """List all running sessions managed in the manager""" + return self.sessions diff --git a/src/plugin.ts b/src/plugin.ts index db765bcb..aefb22a3 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -36,6 +36,8 @@ const extension: JupyterFrontEndPlugin = { widgetExports.ShellModel.shell = shell; widgetExports.CommandRegistryModel.commands = app.commands; widgetExports.CommandPaletteModel.palette = palette; + widgetExports.SessionManagerModel.sessions = app.serviceManager.sessions; + widgetExports.SessionManagerModel.shell = shell; registry.registerWidget({ name: MODULE_NAME, diff --git a/src/widget.ts b/src/widget.ts index f446b797..2e2a3239 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -6,6 +6,7 @@ import '../css/widget.css'; import { CommandRegistryModel } from './widgets/commands'; import { CommandPaletteModel } from './widgets/palette'; +import { SessionManagerModel } from './widgets/sessions'; import { JupyterFrontEndModel } from './widgets/frontend'; import { PanelModel } from './widgets/panel'; import { ShellModel } from './widgets/shell'; @@ -21,4 +22,5 @@ export { SplitPanelModel, SplitPanelView, TitleModel, + SessionManagerModel, }; diff --git a/src/widgets/sessions.ts b/src/widgets/sessions.ts new file mode 100644 index 00000000..0cdcfe3c --- /dev/null +++ b/src/widgets/sessions.ts @@ -0,0 +1,126 @@ +// SessionManager exposes `JupyterLab.serviceManager.sessions` to user python kernel + +import { SessionManager } from '@jupyterlab/services'; +import { ISerializers, WidgetModel } from '@jupyter-widgets/base'; +import { toArray } from '@lumino/algorithm'; +import { MODULE_NAME, MODULE_VERSION } from '../version'; +import { Session } from '@jupyterlab/services'; +import { ILabShell } from '@jupyterlab/application'; + +/** + * The model for a Session Manager + */ +export class SessionManagerModel extends WidgetModel { + /** + * The default attributes. + */ + defaults(): any { + return { + ...super.defaults(), + _model_name: SessionManagerModel.model_name, + _model_module: SessionManagerModel.model_module, + _model_module_version: SessionManagerModel.model_module_version, + current_session: null, + sessions: [], + }; + } + + /** + * Initialize a SessionManagerModel instance. + * + * @param attributes The base attributes. + * @param options The initialization options. + */ + initialize(attributes: any, options: any): void { + const { sessions, shell } = SessionManagerModel; + this._sessions = sessions; + this._shell = shell; + sessions.runningChanged.connect(this._sendSessions, this); + shell.currentChanged.connect(this._currentChanged, this); + + super.initialize(attributes, options); + this.on('msg:custom', this._onMessage.bind(this)); + this._shell.activeChanged.connect(this._currentChanged, this); + this._sendSessions(); + this._sendCurrent(); + this.send({ event: 'sessions_initialized' }, {}); + } + + /** + * Handle a custom message from the backend. + * + * @param msg The message to handle. + */ + private _onMessage(msg: any): void { + switch (msg.func) { + case 'refreshRunning': + this._sessions.refreshRunning().then(() => { + this.send({ event: 'sessions_refreshed' }, {}); + }); + break; + default: + break; + } + } + + /** + * get sessionContext from a given widget instance + * + * @param widget widget tracked by app.shell._track (FocusTracker) + */ + private _getSessionContext(widget: any): Session.IModel | {} { + return widget?.sessionContext?.session?.model ?? {}; + } + + /** + * Handle focus change in JLab + * + * NOTE: currentChange fires on two situations that we are concerned about here: + * 1. when user focuses on a widget in browser, which the `change.newValue` will + * be the current Widget + * 2. when user executes a code in console/notebook, where the `changed.newValue` will be null since + * we lost focus due to execution. + * To solve this problem, we interrogate `this._tracker.currentWidget` directly. + * We also added a simple fencing to reduce the number of Comm sync calls between Python/JS + */ + private _currentChanged(): void { + this._current_session = this._getSessionContext(this._shell.currentWidget); + this.set('current_session', this._current_session); + this.set('sessions', toArray(this._sessions.running())); + this.save_changes(); + } + + /** + * Send the list of sessions to the backend. + */ + private _sendSessions(): void { + this.set('sessions', toArray(this._sessions.running())); + this.save_changes(); + } + + /** + * send current session to backend + */ + private _sendCurrent(): void { + this._current_session = this._getSessionContext(this._shell.currentWidget); + this.set('current_session', this._current_session); + this.save_changes(); + } + + static serializers: ISerializers = { + ...WidgetModel.serializers, + }; + + static model_name = 'SessionManagerModel'; + static model_module = MODULE_NAME; + static model_module_version = MODULE_VERSION; + static view_name: string = null; + static view_module: string = null; + static view_module_version = MODULE_VERSION; + + private _current_session: Session.IModel | {}; + private _sessions: SessionManager; + static sessions: SessionManager; + private _shell: ILabShell; + static shell: ILabShell; +}