utils.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. JSONObject,
  5. JSONPrimitive,
  6. PromiseDelegate,
  7. UUID
  8. } from '@lumino/coreutils';
  9. import { Response } from 'node-fetch';
  10. import encoding from 'text-encoding';
  11. import WebSocket from 'ws';
  12. import {
  13. Contents,
  14. Kernel,
  15. KernelManager,
  16. KernelMessage,
  17. ServerConnection,
  18. Session,
  19. SessionManager,
  20. Terminal
  21. } from '../src';
  22. import { deserialize, serialize } from '../src/kernel/serialize';
  23. // stub for node global
  24. declare let global: any;
  25. /**
  26. * This can be used by test modules that wouldn't otherwise import
  27. * this file.
  28. */
  29. export function init() {
  30. if (typeof global !== 'undefined') {
  31. global.TextEncoder = encoding.TextEncoder;
  32. global.TextDecoder = encoding.TextDecoder;
  33. }
  34. }
  35. // Call init.
  36. init();
  37. /**
  38. * Create a set of server settings.
  39. */
  40. export function makeSettings(
  41. settings?: Partial<ServerConnection.ISettings>
  42. ): ServerConnection.ISettings {
  43. return ServerConnection.makeSettings(settings);
  44. }
  45. const EXAMPLE_KERNEL_INFO: KernelMessage.IInfoReplyMsg['content'] = {
  46. status: 'ok',
  47. protocol_version: '1',
  48. implementation: 'a',
  49. implementation_version: '1',
  50. language_info: {
  51. name: 'test',
  52. version: '',
  53. mimetype: '',
  54. file_extension: '',
  55. pygments_lexer: '',
  56. codemirror_mode: '',
  57. nbconverter_exporter: ''
  58. },
  59. banner: '',
  60. help_links: [
  61. {
  62. text: 'A very helpful link',
  63. url: 'https://very.helpful.website'
  64. }
  65. ]
  66. };
  67. export const PYTHON_SPEC: JSONObject = {
  68. name: 'Python',
  69. spec: {
  70. language: 'python',
  71. argv: [],
  72. display_name: 'python',
  73. env: {}
  74. },
  75. resources: { foo: 'bar' }
  76. };
  77. export const DEFAULT_FILE: Contents.IModel = {
  78. name: 'test',
  79. path: 'foo/test',
  80. type: 'file',
  81. created: 'yesterday',
  82. last_modified: 'today',
  83. writable: true,
  84. mimetype: 'text/plain',
  85. content: 'hello, world!',
  86. format: 'text'
  87. };
  88. export const KERNELSPECS: JSONObject = {
  89. default: 'python',
  90. kernelspecs: {
  91. python: PYTHON_SPEC,
  92. shell: {
  93. name: 'shell',
  94. spec: {
  95. language: 'shell',
  96. argv: [],
  97. display_name: 'Shell',
  98. env: {}
  99. },
  100. resources: {}
  101. }
  102. }
  103. };
  104. /**
  105. * Get a single handler for a request.
  106. */
  107. export function getRequestHandler(
  108. status: number,
  109. body: any
  110. ): ServerConnection.ISettings {
  111. const fetch = (info: RequestInfo, init: RequestInit) => {
  112. // Normalize the body.
  113. body = JSON.stringify(body);
  114. // Create the response and return it as a promise.
  115. const response = new Response(body, { status });
  116. return Promise.resolve(response as any);
  117. };
  118. return ServerConnection.makeSettings({ fetch });
  119. }
  120. /**
  121. * An interface for a service that has server settings.
  122. */
  123. export interface IService {
  124. readonly serverSettings: ServerConnection.ISettings;
  125. }
  126. /**
  127. * Handle a single request with a mock response.
  128. */
  129. export function handleRequest(item: IService, status: number, body: any) {
  130. // Store the existing fetch function.
  131. const oldFetch = item.serverSettings.fetch;
  132. // A single use callback.
  133. const temp = (info: RequestInfo, init: RequestInit) => {
  134. // Restore fetch.
  135. (item.serverSettings as any).fetch = oldFetch;
  136. // Normalize the body.
  137. if (typeof body !== 'string') {
  138. body = JSON.stringify(body);
  139. }
  140. // Create the response and return it as a promise.
  141. const response = new Response(body, { status });
  142. return Promise.resolve(response as any);
  143. };
  144. // Override the fetch function.
  145. (item.serverSettings as any).fetch = temp;
  146. }
  147. /**
  148. * Get a random integer between two values
  149. *
  150. * This implementation comes from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
  151. */
  152. function getRandomInt(min: number, max: number) {
  153. min = Math.ceil(min);
  154. max = Math.floor(max);
  155. return Math.floor(Math.random() * (max - min)) + min; // The maximum is exclusive and the minimum is inclusive
  156. }
  157. /**
  158. * Socket class test rig.
  159. */
  160. class SocketTester implements IService {
  161. /**
  162. * Create a new request and socket tester.
  163. */
  164. constructor() {
  165. let port: number;
  166. // Retry 6 random ports before giving up
  167. for (let retry = 0; retry <= 5; retry++) {
  168. try {
  169. port = getRandomInt(9000, 20000);
  170. this._server = new WebSocket.Server({ port });
  171. } catch (err) {
  172. if (retry === 5) {
  173. throw err;
  174. } else {
  175. continue;
  176. }
  177. }
  178. // We have a successful port
  179. break;
  180. }
  181. this.serverSettings = ServerConnection.makeSettings({
  182. wsUrl: `ws://localhost:${port!}/`,
  183. WebSocket: WebSocket as any
  184. });
  185. this._ready = new PromiseDelegate<void>();
  186. this._server!.on('connection', ws => {
  187. this._ws = ws;
  188. this.onSocket(ws);
  189. this._ready!.resolve();
  190. const connect = this._onConnect;
  191. if (connect) {
  192. connect(ws);
  193. }
  194. });
  195. }
  196. readonly serverSettings: ServerConnection.ISettings;
  197. get ready() {
  198. return this._ready!.promise;
  199. }
  200. /**
  201. * Dispose the socket test rig.
  202. */
  203. dispose(): void {
  204. if (this.isDisposed) {
  205. return;
  206. }
  207. if (this._server) {
  208. this._server.close();
  209. }
  210. this._server = null;
  211. }
  212. /**
  213. * Test if the socket test rig is disposed.
  214. */
  215. get isDisposed(): boolean {
  216. return this._server === null;
  217. }
  218. /**
  219. * Send a raw message from the server to a connected client.
  220. */
  221. sendRaw(msg: string | ArrayBuffer) {
  222. this._ws!.send(msg);
  223. }
  224. /**
  225. * Close the socket.
  226. */
  227. async close(): Promise<void> {
  228. this._ready = new PromiseDelegate<void>();
  229. this._ws!.close();
  230. }
  231. /**
  232. * Register the handler for connections.
  233. */
  234. onConnect(cb: (ws: WebSocket) => void): void {
  235. this._onConnect = cb;
  236. }
  237. /**
  238. * A callback executed when a new server websocket is created.
  239. */
  240. protected onSocket(sock: WebSocket): void {
  241. /* no-op */
  242. }
  243. private _ws: WebSocket | null = null;
  244. private _ready: PromiseDelegate<void> | null = null;
  245. private _server: WebSocket.Server | null = null;
  246. private _onConnect: ((ws: WebSocket) => void) | null = null;
  247. protected settings: ServerConnection.ISettings;
  248. }
  249. export class FakeKernelManager extends KernelManager {
  250. // Override requestRunning since we aren't starting kernels
  251. // on the server.
  252. // This prevents kernel connections from being culled.
  253. requestRunning(): Promise<void> {
  254. return Promise.resolve(void 0);
  255. }
  256. }
  257. /**
  258. * Kernel class test rig.
  259. */
  260. export class KernelTester extends SocketTester {
  261. constructor() {
  262. super();
  263. this._kernelManager = new FakeKernelManager({
  264. serverSettings: this.serverSettings
  265. });
  266. }
  267. initialStatus: Kernel.Status = 'starting';
  268. /**
  269. * The parent header sent on messages.
  270. *
  271. * #### Notes:
  272. * Set to `undefined` to send no parent header.
  273. */
  274. parentHeader: KernelMessage.IHeader | undefined;
  275. /**
  276. * Send the status from the server to the client.
  277. */
  278. sendStatus(msgId: string, status: Kernel.Status) {
  279. return this.sendMessage({
  280. msgId,
  281. msgType: 'status',
  282. channel: 'iopub',
  283. content: { execution_state: status }
  284. });
  285. }
  286. /**
  287. * Send an iopub stream message.
  288. */
  289. sendStream(msgId: string, content: KernelMessage.IStreamMsg['content']) {
  290. return this.sendMessage({
  291. msgId,
  292. msgType: 'stream',
  293. channel: 'iopub',
  294. content
  295. });
  296. }
  297. /**
  298. * Send an iopub display message.
  299. */
  300. sendDisplayData(
  301. msgId: string,
  302. content: KernelMessage.IDisplayDataMsg['content']
  303. ) {
  304. return this.sendMessage({
  305. msgId,
  306. msgType: 'display_data',
  307. channel: 'iopub',
  308. content
  309. });
  310. }
  311. /**
  312. * Send an iopub display message.
  313. */
  314. sendUpdateDisplayData(
  315. msgId: string,
  316. content: KernelMessage.IUpdateDisplayDataMsg['content']
  317. ) {
  318. return this.sendMessage({
  319. msgId,
  320. msgType: 'update_display_data',
  321. channel: 'iopub',
  322. content
  323. });
  324. }
  325. /**
  326. * Send an iopub comm open message.
  327. */
  328. sendCommOpen(msgId: string, content: KernelMessage.ICommOpenMsg['content']) {
  329. return this.sendMessage({
  330. msgId,
  331. msgType: 'comm_open',
  332. channel: 'iopub',
  333. content
  334. });
  335. }
  336. /**
  337. * Send an iopub comm close message.
  338. */
  339. sendCommClose(
  340. msgId: string,
  341. content: KernelMessage.ICommCloseMsg['content']
  342. ) {
  343. return this.sendMessage({
  344. msgId,
  345. msgType: 'comm_close',
  346. channel: 'iopub',
  347. content
  348. });
  349. }
  350. /**
  351. * Send an iopub comm message.
  352. */
  353. sendCommMsg(msgId: string, content: KernelMessage.ICommMsgMsg['content']) {
  354. return this.sendMessage({
  355. msgId,
  356. msgType: 'comm_msg',
  357. channel: 'iopub',
  358. content
  359. });
  360. }
  361. sendExecuteResult(
  362. msgId: string,
  363. content: KernelMessage.IExecuteResultMsg['content']
  364. ) {
  365. return this.sendMessage({
  366. msgId,
  367. msgType: 'execute_result',
  368. channel: 'iopub',
  369. content
  370. });
  371. }
  372. sendExecuteReply(
  373. msgId: string,
  374. content: KernelMessage.IExecuteReplyMsg['content']
  375. ) {
  376. return this.sendMessage({
  377. msgId,
  378. msgType: 'execute_reply',
  379. channel: 'shell',
  380. content
  381. });
  382. }
  383. sendKernelInfoReply(
  384. msgId: string,
  385. content: KernelMessage.IInfoReplyMsg['content']
  386. ) {
  387. return this.sendMessage({
  388. msgId,
  389. msgType: 'kernel_info_reply',
  390. channel: 'shell',
  391. content
  392. });
  393. }
  394. sendInputRequest(
  395. msgId: string,
  396. content: KernelMessage.IInputRequestMsg['content']
  397. ) {
  398. return this.sendMessage({
  399. msgId,
  400. msgType: 'input_request',
  401. channel: 'stdin',
  402. content
  403. });
  404. }
  405. /**
  406. * Send a kernel message with sensible defaults.
  407. */
  408. sendMessage<T extends KernelMessage.Message>(
  409. options: MakeOptional<KernelMessage.IOptions<T>, 'session'>
  410. ) {
  411. const msg = KernelMessage.createMessage<any>({
  412. session: this.serverSessionId,
  413. ...options
  414. });
  415. msg.parent_header = this.parentHeader;
  416. this.send(msg);
  417. return msg.header.msg_id;
  418. }
  419. /**
  420. * Send a kernel message from the server to the client.
  421. */
  422. send(msg: KernelMessage.Message): void {
  423. this.sendRaw(serialize(msg));
  424. }
  425. /**
  426. * Start a client-side kernel talking to our websocket server.
  427. */
  428. async start(): Promise<Kernel.IKernelConnection> {
  429. // Set up the kernel request response.
  430. handleRequest(this, 201, { name: 'test', id: UUID.uuid4() });
  431. // Construct a new kernel.
  432. this._kernel = await this._kernelManager.startNew();
  433. // Wait for the other side to signal it is connected
  434. await this.ready;
  435. // Wait for the initial kernel info reply if we have a normal status
  436. if (this.initialStatus === 'starting') {
  437. await this._kernel.info;
  438. }
  439. return this._kernel;
  440. }
  441. /**
  442. * Shut down the current kernel
  443. */
  444. async shutdown(): Promise<void> {
  445. if (this._kernel && !this._kernel.isDisposed) {
  446. // Set up the kernel request response.
  447. handleRequest(this, 204, {});
  448. await this._kernel.shutdown();
  449. }
  450. }
  451. /**
  452. * Register the message callback with the websocket server.
  453. */
  454. onMessage(cb: (msg: KernelMessage.IMessage) => void): void {
  455. this._onMessage = cb;
  456. }
  457. /**
  458. * Dispose the tester.
  459. */
  460. dispose() {
  461. if (this._kernel) {
  462. this._kernel.dispose();
  463. this._kernel = null;
  464. }
  465. super.dispose();
  466. }
  467. /**
  468. * Set up a new server websocket to pretend like it is a server kernel.
  469. */
  470. protected onSocket(sock: WebSocket): void {
  471. super.onSocket(sock);
  472. this.sendStatus(UUID.uuid4(), this.initialStatus);
  473. sock.on('message', (msg: any) => {
  474. if (msg instanceof Buffer) {
  475. msg = new Uint8Array(msg).buffer;
  476. }
  477. const data = deserialize(msg);
  478. if (data.header.msg_type === 'kernel_info_request') {
  479. // First send status busy message.
  480. this.parentHeader = data.header;
  481. this.sendStatus(UUID.uuid4(), 'busy');
  482. // Then send the kernel_info_reply message.
  483. this.sendKernelInfoReply(UUID.uuid4(), EXAMPLE_KERNEL_INFO);
  484. // Then send status idle message.
  485. this.sendStatus(UUID.uuid4(), 'idle');
  486. this.parentHeader = undefined;
  487. } else {
  488. const onMessage = this._onMessage;
  489. if (onMessage) {
  490. onMessage(data);
  491. }
  492. }
  493. });
  494. }
  495. readonly serverSessionId = UUID.uuid4();
  496. private _kernelManager: Kernel.IManager;
  497. private _kernel: Kernel.IKernelConnection | null = null;
  498. private _onMessage: ((msg: KernelMessage.IMessage) => void) | null = null;
  499. }
  500. /**
  501. * Create a unique session id.
  502. */
  503. export function createSessionModel(id?: string): Session.IModel {
  504. return {
  505. id: id || UUID.uuid4(),
  506. path: UUID.uuid4(),
  507. name: '',
  508. type: '',
  509. kernel: { id: UUID.uuid4(), name: UUID.uuid4() }
  510. };
  511. }
  512. /**
  513. * Session test rig.
  514. */
  515. export class SessionTester extends SocketTester {
  516. constructor() {
  517. super();
  518. const kernelManager = new KernelManager({
  519. serverSettings: this.serverSettings
  520. });
  521. this._sessionManager = new SessionManager({ kernelManager });
  522. }
  523. initialStatus: Kernel.Status = 'starting';
  524. /**
  525. * Start a mock session.
  526. */
  527. async startSession(): Promise<Session.ISessionConnection> {
  528. handleRequest(this, 201, createSessionModel());
  529. this._session = await this._sessionManager!.startNew({
  530. path: UUID.uuid4(),
  531. name: UUID.uuid4(),
  532. type: 'test'
  533. });
  534. await this.ready;
  535. return this._session;
  536. }
  537. /**
  538. * Shut down the current session
  539. */
  540. async shutdown(): Promise<void> {
  541. if (this._session) {
  542. // Set up the session request response.
  543. handleRequest(this, 204, {});
  544. await this._session.shutdown();
  545. }
  546. }
  547. dispose(): void {
  548. super.dispose();
  549. if (this._session) {
  550. this._session.dispose();
  551. this._session = null!;
  552. }
  553. }
  554. /**
  555. * Send the status from the server to the client.
  556. */
  557. sendStatus(status: Kernel.Status, parentHeader?: KernelMessage.IHeader) {
  558. const msg = KernelMessage.createMessage({
  559. msgType: 'status',
  560. channel: 'iopub',
  561. session: this.serverSessionId,
  562. content: {
  563. execution_state: status
  564. }
  565. });
  566. if (parentHeader) {
  567. msg.parent_header = parentHeader;
  568. }
  569. this.send(msg);
  570. }
  571. /**
  572. * Send a kernel message from the server to the client.
  573. */
  574. send(msg: KernelMessage.IMessage): void {
  575. this.sendRaw(serialize(msg));
  576. }
  577. /**
  578. * Register the message callback with the websocket server.
  579. */
  580. onMessage(cb: (msg: KernelMessage.IMessage) => void): void {
  581. this._onMessage = cb;
  582. }
  583. /**
  584. * Set up a new server websocket to pretend like it is a server kernel.
  585. */
  586. protected onSocket(sock: WebSocket): void {
  587. super.onSocket(sock);
  588. sock.on('message', (msg: any) => {
  589. if (msg instanceof Buffer) {
  590. msg = new Uint8Array(msg).buffer;
  591. }
  592. const data = deserialize(msg);
  593. if (KernelMessage.isInfoRequestMsg(data)) {
  594. // First send status busy message.
  595. this.sendStatus('busy', data.header);
  596. // Then send the kernel_info_reply message.
  597. const reply = KernelMessage.createMessage({
  598. msgType: 'kernel_info_reply',
  599. channel: 'shell',
  600. session: this.serverSessionId,
  601. content: EXAMPLE_KERNEL_INFO
  602. });
  603. reply.parent_header = data.header;
  604. this.send(reply);
  605. // Then send status idle message.
  606. this.sendStatus('idle', data.header);
  607. } else {
  608. const onMessage = this._onMessage;
  609. if (onMessage) {
  610. onMessage(data);
  611. }
  612. }
  613. });
  614. }
  615. readonly serverSessionId = UUID.uuid4();
  616. private _session: Session.ISessionConnection;
  617. private _onMessage: ((msg: KernelMessage.IMessage) => void) | null = null;
  618. private _sessionManager: Session.IManager | null = null;
  619. }
  620. /**
  621. * Terminal session test rig.
  622. */
  623. export class TerminalTester extends SocketTester {
  624. /**
  625. * Register the message callback with the websocket server.
  626. */
  627. onMessage(cb: (msg: Terminal.IMessage) => void) {
  628. this._onMessage = cb;
  629. }
  630. protected onSocket(sock: WebSocket): void {
  631. super.onSocket(sock);
  632. sock.on('message', (msg: any) => {
  633. const onMessage = this._onMessage;
  634. if (onMessage) {
  635. const data = JSON.parse(msg) as JSONPrimitive[];
  636. const termMsg: Terminal.IMessage = {
  637. type: data[0] as Terminal.MessageType,
  638. content: data.slice(1)
  639. };
  640. onMessage(termMsg);
  641. }
  642. });
  643. }
  644. private _onMessage: ((msg: Terminal.IMessage) => void) | null = null;
  645. }
  646. /**
  647. * Make a new type with the given keys declared as optional.
  648. *
  649. * #### Notes
  650. * An example:
  651. *
  652. * interface A {a: number, b: string}
  653. * type B = MakeOptional<A, 'a'>
  654. * const x: B = {b: 'test'}
  655. */
  656. type MakeOptional<T, K> = Pick<T, Exclude<keyof T, K>> &
  657. { [P in Extract<keyof T, K>]?: T[P] };