Skip to content

Commit dac0e82

Browse files
committed
Support long execution and (not working stdin)
1 parent 536a7b8 commit dac0e82

File tree

3 files changed

+178
-5
lines changed

3 files changed

+178
-5
lines changed

packages/docprovider/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@
4646
"@jupyterlab/cells": "^4.2.0",
4747
"@jupyterlab/coreutils": "^6.2.0",
4848
"@jupyterlab/notebook": "^4.2.0",
49+
"@jupyterlab/outputarea": "^4.2.0",
4950
"@jupyterlab/services": "^7.2.0",
5051
"@jupyterlab/translation": "^4.2.0",
5152
"@lumino/coreutils": "^2.1.0",
5253
"@lumino/disposable": "^2.1.0",
5354
"@lumino/signaling": "^2.1.0",
55+
"@lumino/widgets": "^2.2.0",
5456
"y-protocols": "^1.0.5",
5557
"y-websocket": "^1.3.15",
5658
"yjs": "^13.5.40"

packages/docprovider/src/notebookCellExecutor.ts

Lines changed: 173 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@
33
| Distributed under the terms of the Modified BSD License.
44
|----------------------------------------------------------------------------*/
55

6-
import { type ICodeCellModel, type MarkdownCell } from '@jupyterlab/cells';
6+
import {
7+
CodeCell,
8+
type ICodeCellModel,
9+
type MarkdownCell
10+
} from '@jupyterlab/cells';
711
import { URLExt } from '@jupyterlab/coreutils';
812
import { INotebookCellExecutor } from '@jupyterlab/notebook';
13+
import { OutputPrompt, Stdin } from '@jupyterlab/outputarea';
914
import { Contents, ServerConnection } from '@jupyterlab/services';
10-
import { nullTranslator } from '@jupyterlab/translation';
15+
import * as KernelMessage from '@jupyterlab/services/lib/kernel/messages';
16+
import { nullTranslator, type ITranslator } from '@jupyterlab/translation';
1117
import { ICollaborativeDrive } from './tokens';
1218
import { Dialog, showDialog } from '@jupyterlab/apputils';
19+
import { PromiseDelegate } from '@lumino/coreutils';
20+
import { Panel } from '@lumino/widgets';
21+
22+
/**
23+
* Polling interval for accepted execution requests.
24+
*/
25+
const MAX_POLLING_INTERVAL = 1000;
1326

1427
/**
1528
* Notebook cell executor posting a request to the server for execution.
@@ -136,10 +149,12 @@ export class NotebookCellServerExecutor implements INotebookCellExecutor {
136149
let success = false;
137150
try {
138151
// FIXME quid of deletedCells and timing record
139-
const response = await ServerConnection.makeRequest(
152+
const response = await requestServer(
153+
cell as CodeCell,
140154
apiURL,
141155
init,
142-
this._serverSettings
156+
this._serverSettings,
157+
translator
143158
);
144159
const data = await response.json();
145160
success = data['status'] === 'ok';
@@ -166,3 +181,157 @@ export class NotebookCellServerExecutor implements INotebookCellExecutor {
166181
return Promise.resolve(true);
167182
}
168183
}
184+
185+
async function requestServer(
186+
cell: CodeCell,
187+
url: string,
188+
init: RequestInit,
189+
settings: ServerConnection.ISettings,
190+
translator?: ITranslator,
191+
interval = 100
192+
): Promise<Response> {
193+
const promise = new PromiseDelegate<Response>();
194+
ServerConnection.makeRequest(url, init, settings)
195+
.then(async response => {
196+
if (!response.ok) {
197+
promise.reject(await ServerConnection.ResponseError.create(response));
198+
} else if (response.status === 202) {
199+
let redirectUrl = response.headers.get('Location') || url;
200+
201+
if (!redirectUrl.startsWith(settings.baseUrl)) {
202+
redirectUrl = URLExt.join(settings.baseUrl, redirectUrl);
203+
}
204+
205+
setTimeout(
206+
async (
207+
cell: CodeCell,
208+
url: string,
209+
init: RequestInit,
210+
settings: ServerConnection.ISettings,
211+
translator?: ITranslator,
212+
interval?: number
213+
) => {
214+
try {
215+
const response = await requestServer(
216+
cell,
217+
url,
218+
init,
219+
settings,
220+
translator,
221+
interval
222+
);
223+
promise.resolve(response);
224+
} catch (error) {
225+
promise.reject(error);
226+
}
227+
},
228+
interval,
229+
cell,
230+
redirectUrl,
231+
{ method: 'GET' },
232+
settings,
233+
translator,
234+
// Evanescent interval
235+
Math.min(MAX_POLLING_INTERVAL, interval * 2)
236+
);
237+
} else if (response.status === 300) {
238+
let replyUrl = response.headers.get('Location') || '';
239+
240+
if (!replyUrl.startsWith(settings.baseUrl)) {
241+
replyUrl = URLExt.join(settings.baseUrl, replyUrl);
242+
}
243+
const { parent_header, input_request } = await response.json();
244+
// TODO only the client sending the snippet will be prompted for the input
245+
// we can have a deadlock if its connection is lost.
246+
const panel = new Panel();
247+
panel.addClass('jp-OutputArea-child');
248+
panel.addClass('jp-OutputArea-stdin-item');
249+
250+
const prompt = new OutputPrompt();
251+
prompt.addClass('jp-OutputArea-prompt');
252+
panel.addWidget(prompt);
253+
254+
const input = new Stdin({
255+
future: Object.freeze({
256+
sendInputReply: (
257+
content: KernelMessage.IInputReply,
258+
parent_header: KernelMessage.IHeader<'input_request'>
259+
) => {
260+
ServerConnection.makeRequest(
261+
replyUrl,
262+
{ method: 'POST' },
263+
settings
264+
).catch(error => {
265+
console.error(
266+
`Failed to set input to ${JSON.stringify(content)}.`,
267+
error
268+
);
269+
});
270+
}
271+
}) as any,
272+
parent_header,
273+
password: input_request.password,
274+
prompt: input_request.prompt,
275+
translator
276+
});
277+
input.addClass('jp-OutputArea-output');
278+
panel.addWidget(input);
279+
280+
// Get the input node to ensure focus after updating the model upon user reply.
281+
const inputNode = input.node.getElementsByTagName('input')[0];
282+
283+
void input.value.then(value => {
284+
panel.addClass('jp-OutputArea-stdin-hiding');
285+
286+
// FIXME this is not great as the model should not be modified on the client.
287+
// Use stdin as the stream so it does not get combined with stdout.
288+
// Note: because it modifies DOM it may (will) shift focus away from the input node.
289+
cell.outputArea.model.add({
290+
output_type: 'stream',
291+
name: 'stdin',
292+
text: value + '\n'
293+
});
294+
// Refocus the input node after it lost focus due to update of the model.
295+
inputNode.focus();
296+
297+
// Keep the input in view for a little while; this (along refocusing)
298+
// ensures that we can avoid the cell editor stealing the focus, and
299+
// leading to user inadvertently modifying editor content when executing
300+
// consecutive commands in short succession.
301+
window.setTimeout(async () => {
302+
// Tack currently focused element to ensure that it remains on it
303+
// after disposal of the panel with the old input
304+
// (which modifies DOM and can lead to focus jump).
305+
const focusedElement = document.activeElement;
306+
// Dispose the old panel with no longer needed input box.
307+
panel.dispose();
308+
// Refocus the element that was focused before.
309+
if (focusedElement && focusedElement instanceof HTMLElement) {
310+
focusedElement.focus();
311+
}
312+
313+
try {
314+
const response = await requestServer(
315+
cell,
316+
url,
317+
init,
318+
settings,
319+
translator
320+
);
321+
promise.resolve(response);
322+
} catch (error) {
323+
promise.reject(error);
324+
}
325+
}, 500);
326+
});
327+
328+
cell.outputArea.layout.addWidget(panel);
329+
} else {
330+
promise.resolve(response);
331+
}
332+
})
333+
.catch(reason => {
334+
promise.reject(new ServerConnection.NetworkError(reason));
335+
});
336+
return promise.promise;
337+
}

yarn.lock

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2169,12 +2169,14 @@ __metadata:
21692169
"@jupyterlab/cells": ^4.2.0
21702170
"@jupyterlab/coreutils": ^6.2.0
21712171
"@jupyterlab/notebook": ^4.2.0
2172+
"@jupyterlab/outputarea": ^4.2.0
21722173
"@jupyterlab/services": ^7.2.0
21732174
"@jupyterlab/testing": ^4.0.0
21742175
"@jupyterlab/translation": ^4.2.0
21752176
"@lumino/coreutils": ^2.1.0
21762177
"@lumino/disposable": ^2.1.0
21772178
"@lumino/signaling": ^2.1.0
2179+
"@lumino/widgets": ^2.2.0
21782180
"@types/jest": ^29.2.0
21792181
jest: ^29.5.0
21802182
rimraf: ^4.1.2
@@ -3295,7 +3297,7 @@ __metadata:
32953297
languageName: node
32963298
linkType: hard
32973299

3298-
"@lumino/widgets@npm:^1.37.2 || ^2.3.2, @lumino/widgets@npm:^2.1.0, @lumino/widgets@npm:^2.3.2":
3300+
"@lumino/widgets@npm:^1.37.2 || ^2.3.2, @lumino/widgets@npm:^2.1.0, @lumino/widgets@npm:^2.2.0, @lumino/widgets@npm:^2.3.2":
32993301
version: 2.3.2
33003302
resolution: "@lumino/widgets@npm:2.3.2"
33013303
dependencies:

0 commit comments

Comments
 (0)