start_jupyter_server.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. // Copyright (c) Jupyter Development Team.
  2. import { spawn, ChildProcess } from 'child_process';
  3. import * as fs from 'fs';
  4. import * as path from 'path';
  5. import { PageConfig, URLExt } from '@jupyterlab/coreutils';
  6. import { PromiseDelegate, UUID } from '@lumino/coreutils';
  7. import { sleep } from './common';
  8. /**
  9. * A Jupyter Server that runs as a child process.
  10. *
  11. * ### Notes
  12. * There can only be one running server at a time, since
  13. * PageConfig is global. Any classes that use `ServerConnection.ISettings`
  14. * such as `ServiceManager` should be instantiated after the server
  15. * has fully started so they pick up the right `PageConfig`.
  16. *
  17. * #### Example
  18. * ```typescript
  19. * const server = new JupyterServer();
  20. *
  21. * beforeAll(async () => {
  22. * await server.start();
  23. * });
  24. *
  25. * afterAll(async () => {
  26. * await server.shutdown();
  27. * });
  28. * ```
  29. *
  30. */
  31. export class JupyterServer {
  32. /**
  33. * Start the server.
  34. *
  35. * @returns A promise that resolves with the url of the server
  36. *
  37. * @throws Error if another server is still running.
  38. */
  39. async start(): Promise<string> {
  40. if (Private.child !== null) {
  41. throw Error('Previous server was not disposed');
  42. }
  43. const startDelegate = new PromiseDelegate<string>();
  44. const env = {
  45. JUPYTER_CONFIG_DIR: Private.handleConfig(),
  46. JUPYTER_DATA_DIR: Private.handleData(),
  47. JUPYTER_RUNTIME_DIR: Private.mktempDir('jupyter_runtime'),
  48. IPYTHONDIR: Private.mktempDir('ipython'),
  49. PATH: process.env.PATH
  50. };
  51. // Create the child process for the server.
  52. const child = (Private.child = spawn('jupyter-lab', { env }));
  53. let started = false;
  54. // Handle server output.
  55. const handleOutput = (output: string) => {
  56. console.debug(output);
  57. if (started) {
  58. return;
  59. }
  60. const baseUrl = Private.handleStartup(output);
  61. if (baseUrl) {
  62. console.debug('Jupyter Server started');
  63. started = true;
  64. void Private.connect(baseUrl, startDelegate);
  65. }
  66. };
  67. child.stdout.on('data', data => {
  68. handleOutput(String(data));
  69. });
  70. child.stderr.on('data', data => {
  71. handleOutput(String(data));
  72. });
  73. const url = await startDelegate.promise;
  74. return url;
  75. }
  76. /**
  77. * Shut down the server, waiting for it to exit gracefully.
  78. */
  79. async shutdown(): Promise<void> {
  80. if (!Private.child) {
  81. return Promise.resolve(void 0);
  82. }
  83. const stopDelegate = new PromiseDelegate<void>();
  84. const child = Private.child;
  85. child.on('exit', code => {
  86. Private.child = null;
  87. if (code !== null && code !== 0) {
  88. stopDelegate.reject('child process exited with code ' + String(code));
  89. } else {
  90. stopDelegate.resolve(void 0);
  91. }
  92. });
  93. child.kill();
  94. window.setTimeout(() => {
  95. if (Private.child) {
  96. Private.child.kill(9);
  97. }
  98. }, 3000);
  99. return stopDelegate.promise;
  100. }
  101. }
  102. /**
  103. * A namespace for module private data.
  104. */
  105. namespace Private {
  106. export let child: ChildProcess | null = null;
  107. /**
  108. * Make a temporary directory.
  109. *
  110. * @param suffix the last portion of the dir naem.
  111. */
  112. export function mktempDir(suffix: string): string {
  113. const pathPrefix = '/tmp/jupyterServer';
  114. if (!fs.existsSync(pathPrefix)) {
  115. fs.mkdirSync(pathPrefix);
  116. }
  117. return fs.mkdtempSync(`${pathPrefix}/${suffix}`);
  118. }
  119. /**
  120. * Install a spec in the data directory.
  121. */
  122. export function installSpec(dataDir: string, name: string, spec: any): void {
  123. const specDir = path.join(dataDir, 'kernels', name);
  124. fs.mkdirSync(specDir, { recursive: true });
  125. fs.writeFileSync(path.join(specDir, 'kernel.json'), JSON.stringify(spec));
  126. }
  127. /**
  128. * Create and populate a notebook directory.
  129. */
  130. function createNotebookDir(): string {
  131. const nbDir = mktempDir('notebook');
  132. fs.mkdirSync(path.join(nbDir, 'src'));
  133. fs.writeFileSync(path.join(nbDir, 'src', 'temp.txt'), 'hello');
  134. const roFilepath = path.join(nbDir, 'src', 'readonly-temp.txt');
  135. fs.writeFileSync(roFilepath, 'hello from a ready only file', {
  136. mode: 0o444
  137. });
  138. return nbDir;
  139. }
  140. /**
  141. * Create a temporary directory for schemas.
  142. */
  143. function createAppDir(): string {
  144. const appDir = mktempDir('app');
  145. // Add a fake static/index.html for `ensure_app_check()`
  146. fs.mkdirSync(path.join(appDir, 'static'));
  147. fs.writeFileSync(path.join(appDir, 'static', 'index.html'), 'foo');
  148. // Add the apputils schema.
  149. const schemaDir = path.join(appDir, 'schemas');
  150. fs.mkdirSync(schemaDir, { recursive: true });
  151. const extensionDir = path.join(
  152. schemaDir,
  153. '@jupyterlab',
  154. 'apputils-extension'
  155. );
  156. fs.mkdirSync(extensionDir, { recursive: true });
  157. // Get schema content.
  158. const schema = {
  159. title: 'Theme',
  160. description: 'Theme manager settings.',
  161. properties: {
  162. theme: {
  163. type: 'string',
  164. title: 'Selected Theme',
  165. default: 'JupyterLab Light'
  166. }
  167. },
  168. type: 'object'
  169. };
  170. fs.writeFileSync(
  171. path.join(extensionDir, 'themes.json'),
  172. JSON.stringify(schema)
  173. );
  174. return appDir;
  175. }
  176. /**
  177. * Handle configuration.
  178. */
  179. export function handleConfig(): string {
  180. // Set up configuration.
  181. const token = UUID.uuid4();
  182. PageConfig.setOption('token', token);
  183. PageConfig.setOption('terminalsAvailable', 'true');
  184. const configDir = mktempDir('config');
  185. const configPath = path.join(configDir, 'jupyter_server_config.json');
  186. const root_dir = createNotebookDir();
  187. const app_dir = createAppDir();
  188. const user_settings_dir = mktempDir('settings');
  189. const workspaces_dir = mktempDir('workspaces');
  190. const configData = {
  191. LabApp: {
  192. user_settings_dir,
  193. workspaces_dir,
  194. app_dir,
  195. open_browser: false,
  196. log_level: 'DEBUG'
  197. },
  198. ServerApp: {
  199. token,
  200. root_dir,
  201. log_level: 'DEBUG'
  202. },
  203. MultiKernelManager: {
  204. default_kernel_name: 'echo'
  205. },
  206. KernelManager: {
  207. shutdown_wait_time: 1.0
  208. }
  209. };
  210. fs.writeFileSync(configPath, JSON.stringify(configData));
  211. return configDir;
  212. }
  213. /**
  214. * Handle data.
  215. */
  216. export function handleData(): string {
  217. const dataDir = mktempDir('data');
  218. // Install custom specs.
  219. installSpec(dataDir, 'echo', {
  220. argv: [
  221. 'python',
  222. '-m',
  223. 'jupyterlab.tests.echo_kernel',
  224. '-f',
  225. '{connection_file}'
  226. ],
  227. display_name: 'Echo Kernel',
  228. language: 'echo'
  229. });
  230. installSpec(dataDir, 'ipython', {
  231. argv: ['python', '-m', 'ipykernel_launcher', '-f', '{connection_file}'],
  232. display_name: 'Python 3',
  233. language: 'python'
  234. });
  235. return dataDir;
  236. }
  237. /**
  238. * Handle process startup.
  239. *
  240. * @param output the process output
  241. *
  242. * @returns The baseUrl of the server or `null`.
  243. */
  244. export function handleStartup(output: string): string | null {
  245. let baseUrl: string | null = null;
  246. output.split('\n').forEach(line => {
  247. const baseUrlMatch = line.match(/(http:\/\/localhost:\d+\/[^?]*)/);
  248. if (baseUrlMatch) {
  249. baseUrl = baseUrlMatch[1].replace('/lab', '');
  250. PageConfig.setOption('baseUrl', baseUrl);
  251. }
  252. });
  253. return baseUrl;
  254. }
  255. /**
  256. * Connect to the Jupyter server.
  257. */
  258. export async function connect(
  259. baseUrl: string,
  260. startDelegate: PromiseDelegate<string>
  261. ): Promise<void> {
  262. // eslint-disable-next-line
  263. while (true) {
  264. try {
  265. await fetch(URLExt.join(baseUrl, 'api'));
  266. startDelegate.resolve(baseUrl);
  267. return;
  268. } catch (e) {
  269. // spin until we can connect to the server.
  270. console.warn(e);
  271. await sleep(1000);
  272. }
  273. }
  274. }
  275. }