manager.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Poll } from '@jupyterlab/coreutils';
  4. import { ArrayExt, IIterator, iter } from '@phosphor/algorithm';
  5. import { JSONExt } from '@phosphor/coreutils';
  6. import { ISignal, Signal } from '@phosphor/signaling';
  7. import { ServerConnection } from '..';
  8. import { TerminalSession } from './terminal';
  9. /**
  10. * A terminal session manager.
  11. */
  12. export class TerminalManager implements TerminalSession.IManager {
  13. /**
  14. * Construct a new terminal manager.
  15. */
  16. constructor(options: TerminalManager.IOptions = {}) {
  17. this.serverSettings =
  18. options.serverSettings || ServerConnection.makeSettings();
  19. // Check if terminals are available
  20. if (!TerminalSession.isAvailable()) {
  21. this._ready = Promise.reject('Terminals unavailable');
  22. this._ready.catch(_ => undefined);
  23. return;
  24. }
  25. // Initialize internal data then start polling.
  26. this._ready = this.requestRunning()
  27. .then(_ => undefined)
  28. .catch(_ => undefined)
  29. .then(() => {
  30. if (this.isDisposed) {
  31. return;
  32. }
  33. this._isReady = true;
  34. });
  35. // Start polling with exponential backoff.
  36. this._pollModels = new Poll({
  37. auto: false,
  38. factory: () => this.requestRunning(),
  39. frequency: {
  40. interval: 10 * 1000,
  41. backoff: true,
  42. max: 300 * 1000
  43. },
  44. name: `@jupyterlab/services:TerminalManager#models`,
  45. standby: options.standby || 'when-hidden'
  46. });
  47. void this.ready.then(() => {
  48. void this._pollModels.start();
  49. });
  50. }
  51. /**
  52. * A signal emitted when the running terminals change.
  53. */
  54. get runningChanged(): ISignal<this, TerminalSession.IModel[]> {
  55. return this._runningChanged;
  56. }
  57. /**
  58. * A signal emitted when there is a connection failure.
  59. */
  60. get connectionFailure(): ISignal<this, Error> {
  61. return this._connectionFailure;
  62. }
  63. /**
  64. * Test whether the terminal manager is disposed.
  65. */
  66. get isDisposed(): boolean {
  67. return this._isDisposed;
  68. }
  69. /**
  70. * The server settings of the manager.
  71. */
  72. readonly serverSettings: ServerConnection.ISettings;
  73. /**
  74. * Test whether the manager is ready.
  75. */
  76. get isReady(): boolean {
  77. return this._isReady;
  78. }
  79. /**
  80. * Dispose of the resources used by the manager.
  81. */
  82. dispose(): void {
  83. if (this.isDisposed) {
  84. return;
  85. }
  86. this._isDisposed = true;
  87. this._models.length = 0;
  88. this._pollModels.dispose();
  89. Signal.clearData(this);
  90. }
  91. /**
  92. * A promise that fulfills when the manager is ready.
  93. */
  94. get ready(): Promise<void> {
  95. return this._ready;
  96. }
  97. /**
  98. * Whether the terminal service is available.
  99. */
  100. isAvailable(): boolean {
  101. return TerminalSession.isAvailable();
  102. }
  103. /**
  104. * Create an iterator over the most recent running terminals.
  105. *
  106. * @returns A new iterator over the running terminals.
  107. */
  108. running(): IIterator<TerminalSession.IModel> {
  109. return iter(this._models);
  110. }
  111. /**
  112. * Create a new terminal session.
  113. *
  114. * @param options - The options used to connect to the session.
  115. *
  116. * @returns A promise that resolves with the terminal instance.
  117. *
  118. * #### Notes
  119. * The manager `serverSettings` will be used unless overridden in the
  120. * options.
  121. */
  122. async startNew(
  123. options?: TerminalSession.IOptions
  124. ): Promise<TerminalSession.ISession> {
  125. const session = await TerminalSession.startNew(this._getOptions(options));
  126. this._onStarted(session);
  127. return session;
  128. }
  129. /*
  130. * Connect to a running session.
  131. *
  132. * @param name - The name of the target session.
  133. *
  134. * @param options - The options used to connect to the session.
  135. *
  136. * @returns A promise that resolves with the new session instance.
  137. *
  138. * #### Notes
  139. * The manager `serverSettings` will be used unless overridden in the
  140. * options.
  141. */
  142. async connectTo(
  143. name: string,
  144. options?: TerminalSession.IOptions
  145. ): Promise<TerminalSession.ISession> {
  146. const session = await TerminalSession.connectTo(
  147. name,
  148. this._getOptions(options)
  149. );
  150. this._onStarted(session);
  151. return session;
  152. }
  153. /**
  154. * Force a refresh of the running sessions.
  155. *
  156. * #### Notes
  157. * This is intended to be called only in response to a user action,
  158. * since the manager maintains its internal state.
  159. */
  160. async refreshRunning(): Promise<void> {
  161. await this._pollModels.refresh();
  162. await this._pollModels.tick;
  163. }
  164. /**
  165. * Shut down a terminal session by name.
  166. */
  167. async shutdown(name: string): Promise<void> {
  168. const models = this._models;
  169. const sessions = this._sessions;
  170. const index = ArrayExt.findFirstIndex(models, model => model.name === name);
  171. if (index === -1) {
  172. return;
  173. }
  174. // Proactively remove the model.
  175. models.splice(index, 1);
  176. this._runningChanged.emit(models.slice());
  177. // Delete and dispose the session locally.
  178. sessions.forEach(session => {
  179. if (session.name === name) {
  180. sessions.delete(session);
  181. session.dispose();
  182. }
  183. });
  184. // Shut down the remote session.
  185. await TerminalSession.shutdown(name, this.serverSettings);
  186. }
  187. /**
  188. * Shut down all terminal sessions.
  189. *
  190. * @returns A promise that resolves when all of the sessions are shut down.
  191. */
  192. async shutdownAll(): Promise<void> {
  193. // Update the list of models then shut down every session.
  194. try {
  195. await this.requestRunning();
  196. await Promise.all(
  197. this._models.map(({ name }) =>
  198. TerminalSession.shutdown(name, this.serverSettings)
  199. )
  200. );
  201. } finally {
  202. // Dispose every kernel and clear the set.
  203. this._sessions.forEach(session => {
  204. session.dispose();
  205. });
  206. this._sessions.clear();
  207. // Remove all models even if we had an error.
  208. if (this._models.length) {
  209. this._models.length = 0;
  210. this._runningChanged.emit([]);
  211. }
  212. }
  213. }
  214. /**
  215. * Execute a request to the server to poll running terminals and update state.
  216. */
  217. protected async requestRunning(): Promise<void> {
  218. const models = await TerminalSession.listRunning(this.serverSettings).catch(
  219. err => {
  220. if (err instanceof ServerConnection.NetworkError) {
  221. this._connectionFailure.emit(err);
  222. return [] as TerminalSession.IModel[];
  223. }
  224. throw err;
  225. }
  226. );
  227. if (this.isDisposed) {
  228. return;
  229. }
  230. if (!JSONExt.deepEqual(models, this._models)) {
  231. const names = models.map(({ name }) => name);
  232. const sessions = this._sessions;
  233. sessions.forEach(session => {
  234. if (names.indexOf(session.name) === -1) {
  235. session.dispose();
  236. sessions.delete(session);
  237. }
  238. });
  239. this._models = models.slice();
  240. this._runningChanged.emit(models);
  241. }
  242. }
  243. /**
  244. * Get a set of options to pass.
  245. */
  246. private _getOptions(
  247. options: TerminalSession.IOptions = {}
  248. ): TerminalSession.IOptions {
  249. return { ...options, serverSettings: this.serverSettings };
  250. }
  251. /**
  252. * Handle a session starting.
  253. */
  254. private _onStarted(session: TerminalSession.ISession): void {
  255. let name = session.name;
  256. this._sessions.add(session);
  257. let index = ArrayExt.findFirstIndex(
  258. this._models,
  259. value => value.name === name
  260. );
  261. if (index === -1) {
  262. this._models.push(session.model);
  263. this._runningChanged.emit(this._models.slice());
  264. }
  265. session.terminated.connect(() => {
  266. this._onTerminated(name);
  267. });
  268. }
  269. /**
  270. * Handle a session terminating.
  271. */
  272. private _onTerminated(name: string): void {
  273. let index = ArrayExt.findFirstIndex(
  274. this._models,
  275. value => value.name === name
  276. );
  277. if (index !== -1) {
  278. this._models.splice(index, 1);
  279. this._runningChanged.emit(this._models.slice());
  280. }
  281. const sessions = this._sessions;
  282. sessions.forEach(session => {
  283. if (session.name === name) {
  284. sessions.delete(session);
  285. }
  286. });
  287. }
  288. private _isDisposed = false;
  289. private _isReady = false;
  290. private _models: TerminalSession.IModel[] = [];
  291. private _pollModels: Poll;
  292. private _sessions = new Set<TerminalSession.ISession>();
  293. private _ready: Promise<void>;
  294. private _runningChanged = new Signal<this, TerminalSession.IModel[]>(this);
  295. private _connectionFailure = new Signal<this, Error>(this);
  296. }
  297. /**
  298. * The namespace for TerminalManager statics.
  299. */
  300. export namespace TerminalManager {
  301. /**
  302. * The options used to initialize a terminal manager.
  303. */
  304. export interface IOptions {
  305. /**
  306. * The server settings used by the manager.
  307. */
  308. serverSettings?: ServerConnection.ISettings;
  309. /**
  310. * When the manager stops polling the API. Defaults to `when-hidden`.
  311. */
  312. standby?: Poll.Standby;
  313. }
  314. }