default.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. URLExt
  5. } from '@jupyterlab/coreutils';
  6. import {
  7. ArrayExt, each, find
  8. } from '@phosphor/algorithm';
  9. import {
  10. ISignal, Signal
  11. } from '@phosphor/signaling';
  12. import {
  13. Kernel, KernelMessage
  14. } from '../kernel';
  15. import {
  16. ServerConnection
  17. } from '..';
  18. import {
  19. Session
  20. } from './session';
  21. import * as validate
  22. from './validate';
  23. /**
  24. * The url for the session service.
  25. */
  26. const SESSION_SERVICE_URL = 'api/sessions';
  27. /**
  28. * Session object for accessing the session REST api. The session
  29. * should be used to start kernels and then shut them down -- for
  30. * all other operations, the kernel object should be used.
  31. */
  32. export
  33. class DefaultSession implements Session.ISession {
  34. /**
  35. * Construct a new session.
  36. */
  37. constructor(options: Session.IOptions, id: string, kernel: Kernel.IKernel) {
  38. this._id = id;
  39. this._path = options.path;
  40. this._type = options.type || 'file';
  41. this._name = options.name || '';
  42. this.serverSettings = options.serverSettings || ServerConnection.makeSettings();
  43. Private.addRunning(this);
  44. this.setupKernel(kernel);
  45. }
  46. /**
  47. * A signal emitted when the session is shut down.
  48. */
  49. get terminated(): ISignal<this, void> {
  50. return this._terminated;
  51. }
  52. /**
  53. * A signal emitted when the kernel changes.
  54. */
  55. get kernelChanged(): ISignal<this, Kernel.IKernelConnection> {
  56. return this._kernelChanged;
  57. }
  58. /**
  59. * A signal emitted when the kernel status changes.
  60. */
  61. get statusChanged(): ISignal<this, Kernel.Status> {
  62. return this._statusChanged;
  63. }
  64. /**
  65. * A signal emitted for a kernel messages.
  66. */
  67. get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
  68. return this._iopubMessage;
  69. }
  70. /**
  71. * A signal emitted for an unhandled kernel message.
  72. */
  73. get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
  74. return this._unhandledMessage;
  75. }
  76. /**
  77. * A signal emitted when a session property changes.
  78. */
  79. get propertyChanged(): ISignal<this, 'path' | 'name' | 'type'> {
  80. return this._propertyChanged;
  81. }
  82. /**
  83. * Get the session id.
  84. */
  85. get id(): string {
  86. return this._id;
  87. }
  88. /**
  89. * Get the session kernel object.
  90. *
  91. * #### Notes
  92. * This is a read-only property, and can be altered by [changeKernel].
  93. * Use the [statusChanged] and [unhandledMessage] signals on the session
  94. * instead of the ones on the kernel.
  95. */
  96. get kernel() : Kernel.IKernelConnection {
  97. return this._kernel;
  98. }
  99. /**
  100. * Get the session path.
  101. */
  102. get path(): string {
  103. return this._path;
  104. }
  105. /**
  106. * Get the session type.
  107. */
  108. get type(): string {
  109. return this._type;
  110. }
  111. /**
  112. * Get the session name.
  113. */
  114. get name(): string {
  115. return this._name;
  116. }
  117. /**
  118. * Get the model associated with the session.
  119. */
  120. get model(): Session.IModel {
  121. return {
  122. id: this.id,
  123. kernel: this.kernel.model,
  124. path: this._path,
  125. type: this._type,
  126. name: this._name
  127. };
  128. }
  129. /**
  130. * The current status of the session.
  131. *
  132. * #### Notes
  133. * This is a delegate to the kernel status.
  134. */
  135. get status(): Kernel.Status {
  136. return this._kernel ? this._kernel.status : 'dead';
  137. }
  138. /**
  139. * The server settings of the session.
  140. */
  141. readonly serverSettings: ServerConnection.ISettings;
  142. /**
  143. * Test whether the session has been disposed.
  144. */
  145. get isDisposed(): boolean {
  146. return this._isDisposed === true;
  147. }
  148. /**
  149. * Clone the current session with a new clientId.
  150. */
  151. clone(): Promise<Session.ISession> {
  152. return Kernel.connectTo(this.kernel.model, this.serverSettings).then(kernel => {
  153. return new DefaultSession({
  154. path: this._path,
  155. name: this._name,
  156. type: this._type,
  157. serverSettings: this.serverSettings
  158. }, this._id, kernel);
  159. });
  160. }
  161. /**
  162. * Update the session based on a session model from the server.
  163. */
  164. update(model: Session.IModel): Promise<void> {
  165. // Avoid a race condition if we are waiting for a REST call return.
  166. if (this._updating) {
  167. return Promise.resolve(void 0);
  168. }
  169. let oldModel = this.model;
  170. this._path = model.path;
  171. this._name = model.name;
  172. this._type = model.type;
  173. if (this._kernel.isDisposed || model.kernel.id !== this._kernel.id) {
  174. return Kernel.connectTo(model.kernel, this.serverSettings).then(kernel => {
  175. this.setupKernel(kernel);
  176. this._kernelChanged.emit(kernel);
  177. this._handleModelChange(oldModel);
  178. });
  179. }
  180. this._handleModelChange(oldModel);
  181. return Promise.resolve(void 0);
  182. }
  183. /**
  184. * Dispose of the resources held by the session.
  185. */
  186. dispose(): void {
  187. if (this.isDisposed) {
  188. return;
  189. }
  190. this._isDisposed = true;
  191. this._kernel.dispose();
  192. this._statusChanged.emit('dead');
  193. this._terminated.emit(void 0);
  194. Private.removeRunning(this);
  195. Signal.clearData(this);
  196. }
  197. /**
  198. * Change the session path.
  199. *
  200. * @param path - The new session path.
  201. *
  202. * @returns A promise that resolves when the session has renamed.
  203. *
  204. * #### Notes
  205. * This uses the Jupyter REST API, and the response is validated.
  206. * The promise is fulfilled on a valid response and rejected otherwise.
  207. */
  208. setPath(path: string): Promise<void> {
  209. if (this.isDisposed) {
  210. return Promise.reject(new Error('Session is disposed'));
  211. }
  212. let data = JSON.stringify({ path });
  213. return this._patch(data).then(() => { return void 0; });
  214. }
  215. /**
  216. * Change the session name.
  217. */
  218. setName(name: string): Promise<void> {
  219. if (this.isDisposed) {
  220. return Promise.reject(new Error('Session is disposed'));
  221. }
  222. let data = JSON.stringify({ name });
  223. return this._patch(data).then(() => { return void 0; });
  224. }
  225. /**
  226. * Change the session type.
  227. */
  228. setType(type: string): Promise<void> {
  229. if (this.isDisposed) {
  230. return Promise.reject(new Error('Session is disposed'));
  231. }
  232. let data = JSON.stringify({ type });
  233. return this._patch(data).then(() => { return void 0; });
  234. }
  235. /**
  236. * Change the kernel.
  237. *
  238. * @params options - The name or id of the new kernel.
  239. *
  240. * #### Notes
  241. * This shuts down the existing kernel and creates a new kernel,
  242. * keeping the existing session ID and session path.
  243. */
  244. changeKernel(options: Partial<Kernel.IModel>): Promise<Kernel.IKernelConnection> {
  245. if (this.isDisposed) {
  246. return Promise.reject(new Error('Session is disposed'));
  247. }
  248. let data = JSON.stringify({ kernel: options });
  249. if (this._kernel) {
  250. return this._kernel.ready.then(() => {
  251. this._kernel.dispose();
  252. return this._patch(data);
  253. }).then(() => this.kernel);
  254. }
  255. return this._patch(data).then(() => this.kernel);
  256. }
  257. /**
  258. * Kill the kernel and shutdown the session.
  259. *
  260. * @returns - The promise fulfilled on a valid response from the server.
  261. *
  262. * #### Notes
  263. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/sessions), and validates the response.
  264. * Disposes of the session and emits a [sessionDied] signal on success.
  265. */
  266. shutdown(): Promise<void> {
  267. if (this.isDisposed) {
  268. return Promise.reject(new Error('Session is disposed'));
  269. }
  270. return Private.shutdownSession(this.id, this.serverSettings);
  271. }
  272. /**
  273. * Handle connections to a kernel. This method is not meant to be
  274. * subclassed.
  275. */
  276. protected setupKernel(kernel: Kernel.IKernel): void {
  277. this._kernel = kernel;
  278. kernel.statusChanged.connect(this.onKernelStatus, this);
  279. kernel.unhandledMessage.connect(this.onUnhandledMessage, this);
  280. kernel.iopubMessage.connect(this.onIOPubMessage, this);
  281. }
  282. /**
  283. * Handle to changes in the Kernel status.
  284. */
  285. protected onKernelStatus(sender: Kernel.IKernel, state: Kernel.Status) {
  286. this._statusChanged.emit(state);
  287. }
  288. /**
  289. * Handle iopub kernel messages.
  290. */
  291. protected onIOPubMessage(sender: Kernel.IKernel, msg: KernelMessage.IIOPubMessage) {
  292. this._iopubMessage.emit(msg);
  293. }
  294. /**
  295. * Handle unhandled kernel messages.
  296. */
  297. protected onUnhandledMessage(sender: Kernel.IKernel, msg: KernelMessage.IMessage) {
  298. this._unhandledMessage.emit(msg);
  299. }
  300. /**
  301. * Send a PATCH to the server, updating the session path or the kernel.
  302. */
  303. private _patch(body: string): Promise<Session.IModel> {
  304. this._updating = true;
  305. let settings = this.serverSettings;
  306. let url = Private.getSessionUrl(settings.baseUrl, this._id);
  307. let init = {
  308. method: 'PATCH',
  309. body
  310. };
  311. return ServerConnection.makeRequest(url, init, settings).then(response => {
  312. this._updating = false;
  313. if (response.status !== 200) {
  314. throw new ServerConnection.ResponseError(response);
  315. }
  316. return response.json();
  317. }).then(data => {
  318. let model = validate.validateModel(data);
  319. return Private.updateFromServer(model, settings.baseUrl);
  320. }, error => {
  321. this._updating = false;
  322. throw error;
  323. });
  324. }
  325. /**
  326. * Handle a change to the model.
  327. */
  328. private _handleModelChange(oldModel: Session.IModel): void {
  329. if (oldModel.name !== this._name) {
  330. this._propertyChanged.emit('name');
  331. }
  332. if (oldModel.type !== this._type) {
  333. this._propertyChanged.emit('type');
  334. }
  335. if (oldModel.path !== this._path) {
  336. this._propertyChanged.emit('path');
  337. }
  338. }
  339. private _id = '';
  340. private _path = '';
  341. private _name = '';
  342. private _type = '';
  343. private _kernel: Kernel.IKernel;
  344. private _isDisposed = false;
  345. private _updating = false;
  346. private _kernelChanged = new Signal<this, Kernel.IKernelConnection>(this);
  347. private _statusChanged = new Signal<this, Kernel.Status>(this);
  348. private _iopubMessage = new Signal<this, KernelMessage.IIOPubMessage>(this);
  349. private _unhandledMessage = new Signal<this, KernelMessage.IMessage>(this);
  350. private _propertyChanged = new Signal<this, 'path' | 'name' | 'type'>(this);
  351. private _terminated = new Signal<this, void>(this);
  352. }
  353. /**
  354. * The namespace for `DefaultSession` statics.
  355. */
  356. export
  357. namespace DefaultSession {
  358. /**
  359. * List the running sessions.
  360. */
  361. export
  362. function listRunning(settings?: ServerConnection.ISettings): Promise<Session.IModel[]> {
  363. return Private.listRunning(settings);
  364. }
  365. /**
  366. * Start a new session.
  367. */
  368. export
  369. function startNew(options: Session.IOptions): Promise<Session.ISession> {
  370. return Private.startNew(options);
  371. }
  372. /**
  373. * Find a session by id.
  374. */
  375. export
  376. function findById(id: string, settings?: ServerConnection.ISettings): Promise<Session.IModel> {
  377. return Private.findById(id, settings);
  378. }
  379. /**
  380. * Find a session by path.
  381. */
  382. export
  383. function findByPath(path: string, settings?: ServerConnection.ISettings): Promise<Session.IModel> {
  384. return Private.findByPath(path, settings);
  385. }
  386. /**
  387. * Connect to a running session.
  388. */
  389. export
  390. function connectTo(model: Session.IModel, settings?: ServerConnection.ISettings): Promise<Session.ISession> {
  391. return Private.connectTo(model, settings);
  392. }
  393. /**
  394. * Shut down a session by id.
  395. */
  396. export
  397. function shutdown(id: string, settings?: ServerConnection.ISettings): Promise<void> {
  398. return Private.shutdownSession(id, settings);
  399. }
  400. /**
  401. * Shut down all sessions.
  402. *
  403. * @param settings - The server settings to use.
  404. *
  405. * @returns A promise that resolves when all the sessions are shut down.
  406. */
  407. export
  408. function shutdownAll(settings?: ServerConnection.ISettings): Promise<void> {
  409. return Private.shutdownAll(settings);
  410. }
  411. }
  412. /**
  413. * A namespace for session private data.
  414. */
  415. namespace Private {
  416. /**
  417. * The running sessions mapped by base url.
  418. */
  419. const runningSessions = new Map<string, DefaultSession[]>();
  420. /**
  421. * Add a session to the running sessions.
  422. */
  423. export
  424. function addRunning(session: DefaultSession): void {
  425. let running: DefaultSession[] = (
  426. runningSessions.get(session.serverSettings.baseUrl) || []
  427. );
  428. running.push(session);
  429. runningSessions.set(session.serverSettings.baseUrl, running);
  430. }
  431. /**
  432. * Remove a session from the running sessions.
  433. */
  434. export
  435. function removeRunning(session: DefaultSession): void {
  436. let running = runningSessions.get(session.serverSettings.baseUrl);
  437. if (running) {
  438. ArrayExt.removeFirstOf(running, session);
  439. }
  440. }
  441. /**
  442. * Connect to a running session.
  443. */
  444. export
  445. function connectTo(model: Session.IModel, settings?: ServerConnection.ISettings): Promise<Session.ISession> {
  446. settings = settings || ServerConnection.makeSettings();
  447. let running = runningSessions.get(settings.baseUrl) || [];
  448. let session = find(running, value => value.id === model.id);
  449. if (session) {
  450. return Promise.resolve(session.clone());
  451. }
  452. return createSession(model, settings);
  453. }
  454. /**
  455. * Create a Session object.
  456. *
  457. * @returns - A promise that resolves with a started session.
  458. */
  459. export
  460. function createSession(model: Session.IModel, settings?: ServerConnection.ISettings): Promise<DefaultSession> {
  461. settings = settings || ServerConnection.makeSettings();
  462. return Kernel.connectTo(model.kernel, settings).then(kernel => {
  463. return new DefaultSession({
  464. path: model.path,
  465. type: model.type,
  466. name: model.name,
  467. serverSettings: settings
  468. }, model.id, kernel);
  469. });
  470. }
  471. /**
  472. * Find a session by id.
  473. */
  474. export
  475. function findById(id: string, settings?: ServerConnection.ISettings): Promise<Session.IModel> {
  476. settings = settings || ServerConnection.makeSettings();
  477. let running = runningSessions.get(settings.baseUrl) || [];
  478. let session = find(running, value => value.id === id);
  479. if (session) {
  480. return Promise.resolve(session.model);
  481. }
  482. return getSessionModel(id, settings).catch(() => {
  483. throw new Error(`No running session for id: ${id}`);
  484. });
  485. }
  486. /**
  487. * Find a session by path.
  488. */
  489. export
  490. function findByPath(path: string, settings?: ServerConnection.ISettings): Promise<Session.IModel> {
  491. settings = settings || ServerConnection.makeSettings();
  492. let running = runningSessions.get(settings.baseUrl) || [];
  493. let session = find(running, value => value.path === path);
  494. if (session) {
  495. return Promise.resolve(session.model);
  496. }
  497. return listRunning(settings).then(models => {
  498. let model = find(models, value => {
  499. return value.path === path;
  500. });
  501. if (model) {
  502. return model;
  503. }
  504. throw new Error(`No running session for path: ${path}`);
  505. });
  506. }
  507. /**
  508. * Get a full session model from the server by session id string.
  509. */
  510. export
  511. function getSessionModel(id: string, settings?: ServerConnection.ISettings): Promise<Session.IModel> {
  512. settings = settings || ServerConnection.makeSettings();
  513. let url = getSessionUrl(settings.baseUrl, id);
  514. return ServerConnection.makeRequest(url, {}, settings).then(response => {
  515. if (response.status !== 200) {
  516. throw new ServerConnection.ResponseError(response);
  517. }
  518. return response.json();
  519. }).then(data => {
  520. validate.validateModel(data);
  521. return updateFromServer(data, settings!.baseUrl);
  522. });
  523. }
  524. /**
  525. * Get a session url.
  526. */
  527. export
  528. function getSessionUrl(baseUrl: string, id: string): string {
  529. return URLExt.join(baseUrl, SESSION_SERVICE_URL, id);
  530. }
  531. /**
  532. * Kill the sessions by id.
  533. */
  534. function killSessions(id: string, baseUrl: string): void {
  535. let running = runningSessions.get(baseUrl) || [];
  536. each(running.slice(), session => {
  537. if (session.id === id) {
  538. session.dispose();
  539. }
  540. });
  541. }
  542. /**
  543. * List the running sessions.
  544. */
  545. export
  546. function listRunning(settings?: ServerConnection.ISettings): Promise<Session.IModel[]> {
  547. settings = settings || ServerConnection.makeSettings();
  548. let url = URLExt.join(settings.baseUrl, SESSION_SERVICE_URL);
  549. return ServerConnection.makeRequest(url, {}, settings).then(response => {
  550. if (response.status !== 200) {
  551. throw new ServerConnection.ResponseError(response);
  552. }
  553. return response.json();
  554. }).then(data => {
  555. if (!Array.isArray(data)) {
  556. throw new Error('Invalid Session list');
  557. }
  558. for (let i = 0; i < data.length; i++) {
  559. validate.validateModel(data[i]);
  560. }
  561. return updateRunningSessions(data, settings!.baseUrl);
  562. });
  563. }
  564. /**
  565. * Shut down a session by id.
  566. */
  567. export
  568. function shutdownSession(id: string, settings?: ServerConnection.ISettings): Promise<void> {
  569. settings = settings || ServerConnection.makeSettings();
  570. let url = getSessionUrl(settings.baseUrl, id);
  571. let init = { method: 'DELETE' };
  572. return ServerConnection.makeRequest(url, init, settings).then(response => {
  573. if (response.status === 404) {
  574. response.json().then(data => {
  575. let msg = (
  576. data.message || `The session "${id}"" does not exist on the server`
  577. );
  578. console.warn(msg);
  579. });
  580. } else if (response.status === 410) {
  581. throw new ServerConnection.ResponseError(response,
  582. 'The kernel was deleted but the session was not'
  583. );
  584. } else if (response.status !== 204) {
  585. throw new ServerConnection.ResponseError(response);
  586. }
  587. killSessions(id, settings!.baseUrl);
  588. });
  589. }
  590. /**
  591. * Shut down all sessions.
  592. */
  593. export
  594. function shutdownAll(settings?: ServerConnection.ISettings): Promise<void> {
  595. settings = settings || ServerConnection.makeSettings();
  596. return listRunning(settings).then(running => {
  597. each(running, s => {
  598. shutdownSession(s.id, settings);
  599. });
  600. });
  601. }
  602. /**
  603. * Start a new session.
  604. */
  605. export
  606. function startNew(options: Session.IOptions): Promise<Session.ISession> {
  607. if (options.path === void 0) {
  608. return Promise.reject(new Error('Must specify a path'));
  609. }
  610. return startSession(options).then(model => {
  611. return createSession(model, options.serverSettings);
  612. });
  613. }
  614. /**
  615. * Create a new session, or return an existing session if a session if
  616. * the session path already exists
  617. */
  618. export
  619. function startSession(options: Session.IOptions): Promise<Session.IModel> {
  620. let settings = options.serverSettings || ServerConnection.makeSettings();
  621. let model = {
  622. kernel: { name: options.kernelName, id: options.kernelId },
  623. path: options.path,
  624. type: options.type || '',
  625. name: options.name || ''
  626. };
  627. let url = URLExt.join(settings.baseUrl, SESSION_SERVICE_URL);
  628. let init = {
  629. method: 'POST',
  630. body: JSON.stringify(model)
  631. };
  632. return ServerConnection.makeRequest(url, init, settings).then(response => {
  633. if (response.status !== 201) {
  634. throw new ServerConnection.ResponseError(response);
  635. }
  636. return response.json();
  637. }).then(data => {
  638. validate.validateModel(data);
  639. return updateFromServer(data, settings.baseUrl);
  640. });
  641. }
  642. /**
  643. * Update the running sessions given an updated session Id.
  644. */
  645. export
  646. function updateFromServer(model: Session.IModel, baseUrl: string): Promise<Session.IModel> {
  647. let promises: Promise<void>[] = [];
  648. let running = runningSessions.get(baseUrl) || [];
  649. each(running.slice(), session => {
  650. if (session.id === model.id) {
  651. promises.push(session.update(model));
  652. }
  653. });
  654. return Promise.all(promises).then(() => { return model; });
  655. }
  656. /**
  657. * Update the running sessions based on new data from the server.
  658. */
  659. export
  660. function updateRunningSessions(sessions: Session.IModel[], baseUrl: string): Promise<Session.IModel[]> {
  661. let promises: Promise<void>[] = [];
  662. let running = runningSessions.get(baseUrl) || [];
  663. each(running.slice(), session => {
  664. let updated = find(sessions, sId => {
  665. if (session.id === sId.id) {
  666. promises.push(session.update(sId));
  667. return true;
  668. }
  669. return false;
  670. });
  671. // If session is no longer running on disk, emit dead signal.
  672. if (!updated && session.status !== 'dead') {
  673. session.dispose();
  674. }
  675. });
  676. return Promise.all(promises).then(() => { return sessions; });
  677. }
  678. }