Skip to content

Commit 2f2f629

Browse files
committed
feat(notebook): implemented interupt execution and restart kernel
1 parent 8adad45 commit 2f2f629

File tree

10 files changed

+139
-38
lines changed

10 files changed

+139
-38
lines changed

package.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,16 @@
13371337
"shortTitle": "Robot Framework Notebook",
13381338
"category": "RobotCode",
13391339
"command": "robotcode.createNewNotebook"
1340+
},
1341+
{
1342+
"command": "robotcode.notebookEditor.restartKernel",
1343+
"title": "Restart Kernel",
1344+
"category": "RobotCode",
1345+
"shortTitle": "Restart",
1346+
"icon": {
1347+
"dark": "./resources/dark/restart-kernel.svg",
1348+
"light": "./resources/light/restart-kernel.svg"
1349+
}
13401350
}
13411351
],
13421352
"menus": {
@@ -1405,6 +1415,13 @@
14051415
"group": "notebook",
14061416
"when": "!virtualWorkspace"
14071417
}
1418+
],
1419+
"notebook/toolbar": [
1420+
{
1421+
"command": "robotcode.notebookEditor.restartKernel",
1422+
"group": "navigation/execute@1",
1423+
"when": "notebookKernel =~ /robotframework-repl/ && isWorkspaceTrusted"
1424+
}
14081425
]
14091426
},
14101427
"breakpoints": [
@@ -1926,4 +1943,4 @@
19261943
"workspaces": [
19271944
"docs"
19281945
]
1929-
}
1946+
}

packages/repl/src/robotcode/repl/base_interpreter.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
import signal
23
from datetime import datetime
34
from pathlib import Path
45
from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Union, cast
@@ -9,7 +10,7 @@
910
from robot.output import Message as OutputMessage
1011
from robot.running import Keyword, TestCase, TestSuite
1112
from robot.running.context import EXECUTION_CONTEXTS
12-
from robot.running.signalhandler import _StopSignalMonitor
13+
from robot.running.signalhandler import STOP_SIGNAL_MONITOR, _StopSignalMonitor
1314

1415
from robotcode.core.utils.path import normalized_path
1516
from robotcode.robot.utils import get_robot_version
@@ -19,18 +20,33 @@
1920
from robot import result, running
2021

2122

23+
class ExecutionInterrupted(ExecutionStatus):
24+
pass
25+
26+
2227
def _register_signal_handler(self: Any, exsignum: Any) -> None:
2328
pass
2429

2530

31+
def _stop_signal_monitor_call(self: Any, signum: Any, frame: Any) -> None:
32+
if self._running_keyword:
33+
self._stop_execution_gracefully()
34+
35+
36+
def _stop_signal_monitor_stop_execution_gracefully(self: Any) -> None:
37+
raise ExecutionInterrupted("Execution interrupted")
38+
39+
2640
_patched = False
2741

2842

2943
def _patch() -> None:
3044
global _patched
3145
if not _patched:
3246
# Monkey patching the _register_signal_handler method to disable robot's signal handling
33-
_StopSignalMonitor._register_signal_handler = _register_signal_handler
47+
# _StopSignalMonitor._register_signal_handler = _register_signal_handler
48+
_StopSignalMonitor.__call__ = _stop_signal_monitor_call
49+
_StopSignalMonitor._stop_execution_gracefully = _stop_signal_monitor_stop_execution_gracefully
3450

3551
_patched = True
3652

@@ -167,7 +183,10 @@ def run_keyword(self, kw: Keyword) -> Any:
167183
except ExecutionStatus:
168184
raise
169185
except BaseException as e:
170-
self.log_message(str(e), "ERROR", timestamp=datetime.now()) # noqa: DTZ005
186+
self.log_message(f"{type(e)}: {e}", "ERROR", timestamp=datetime.now()) # noqa: DTZ005
187+
188+
def interrupt(self) -> None:
189+
signal.raise_signal(signal.SIGINT)
171190

172191
def run(self) -> Any:
173192
self._logger.enabled = True
@@ -181,6 +200,8 @@ def run(self) -> Any:
181200
break
182201
except (SystemExit, KeyboardInterrupt):
183202
break
203+
except ExecutionInterrupted as e:
204+
self.log_message(str(e), "ERROR", timestamp=datetime.now()) # noqa: DTZ005
184205
except ExecutionStatus:
185206
pass
186207
except BaseException as e:
@@ -190,9 +211,10 @@ def run(self) -> Any:
190211

191212
def run_input(self) -> None:
192213
for kw in self.get_input():
193-
if kw is None:
194-
break
195-
self.set_last_result(self.run_keyword(kw))
214+
with STOP_SIGNAL_MONITOR:
215+
if kw is None:
216+
break
217+
self.set_last_result(self.run_keyword(kw))
196218

197219
def set_last_result(self, result: Any) -> None:
198220
self.last_result = result

packages/repl_server/src/robotcode/repl_server/interpreter.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,14 @@ def __init__(
108108
self.files = files
109109
self.has_input = Event()
110110
self.executed = Event()
111+
self.no_execution = Event()
112+
self.no_execution.set()
111113
self._code: List[str] = []
112114
self._success: Optional[bool] = None
113115
self._result_data: Optional[ResultData] = None
114116
self._result_data_stack: List[ResultData] = []
115117
self.collect_messages: bool = False
118+
self._interrupted = False
116119
self._has_shutdown = False
117120
self._cell_errors: List[str] = []
118121

@@ -122,11 +125,17 @@ def shutdown(self) -> None:
122125
self.has_input.set()
123126

124127
def execute(self, source: str) -> ExecutionResult:
128+
self.no_execution.wait()
129+
130+
self.no_execution.clear()
131+
125132
self._result_data_stack = []
126133

127134
self._success = None
128135
try:
129136
self._cell_errors = []
137+
self._interrupted = False
138+
130139
self._result_data = RootResultData()
131140

132141
self.executed.clear()
@@ -159,6 +168,8 @@ def execute(self, source: str) -> ExecutionResult:
159168
)
160169
except BaseException as e:
161170
return ExecutionResult(False, [ExecutionOutput("application/vnd.code.notebook.stderr", str(e))])
171+
finally:
172+
self.no_execution.set()
162173

163174
def get_input(self) -> Iterator[Optional[Keyword]]:
164175
while self._code:

packages/repl_server/src/robotcode/repl_server/protocol.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ def initialize(self, message: str) -> str:
1717
return "yeah initialized " + message
1818

1919
@rpc_method(name="executeCell", threaded=True)
20-
def execute_cell(self, source: str) -> Optional[ExecutionResult]:
20+
def execute_cell(self, source: str, language_id: str) -> Optional[ExecutionResult]:
2121
return self.interpreter.execute(source)
2222

23+
@rpc_method(name="interrupt", threaded=True)
24+
def interrupt(self) -> None:
25+
self.interpreter.interrupt()
26+
2327
@rpc_method(name="shutdown", threaded=True)
2428
def shutdown(self) -> None:
2529
try:

resources/dark/restart-kernel.svg

Lines changed: 3 additions & 0 deletions
Loading

resources/light/restart-kernel.svg

Lines changed: 3 additions & 0 deletions
Loading

vscode-client/extension/languageToolsManager.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,14 @@ export class LanguageToolsManager {
234234
}
235235
if (folder === undefined) return;
236236

237-
const { pythonCommand, final_args } = await this.pythonManager.buildRobotCodeCommand(folder, ["repl"]);
237+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION, folder);
238+
const profiles = config.get<string[]>("profiles", []);
239+
240+
const { pythonCommand, final_args } = await this.pythonManager.buildRobotCodeCommand(
241+
folder,
242+
["repl"],
243+
profiles,
244+
);
238245
vscode.window
239246
.createTerminal({
240247
name: `Robot REPL${vscode.workspace.workspaceFolders?.length === 1 ? "" : ` (${folder.name})`}`,

vscode-client/extension/notebook.ts

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PythonManager } from "./pythonmanger";
66
import * as cp from "child_process";
77
import * as rpc from "vscode-jsonrpc/node";
88
import { withTimeout } from "./utils";
9+
import { CONFIG_SECTION } from "./config";
910

1011
interface RawNotebook {
1112
cells: RawNotebookCell[];
@@ -161,7 +162,7 @@ export class ReplServerClient {
161162

162163
connection: rpc.MessageConnection | undefined;
163164
childProcess: cp.ChildProcessWithoutNullStreams | undefined;
164-
private _cancelationTokenSource: vscode.CancellationTokenSource | undefined;
165+
private _cancelationTokenSources = new Map<vscode.CancellationTokenSource, vscode.NotebookCell>();
165166

166167
dispose(): void {
167168
this.exitClient().finally(() => {});
@@ -198,10 +199,11 @@ export class ReplServerClient {
198199
}
199200

200201
cancelCurrentExecution(): void {
201-
if (this._cancelationTokenSource) {
202-
this._cancelationTokenSource.cancel();
203-
this._cancelationTokenSource.dispose();
202+
for (const [token] of this._cancelationTokenSources) {
203+
token.cancel();
204+
token.dispose();
204205
}
206+
this._cancelationTokenSources.clear();
205207
}
206208

207209
async ensureInitialized(): Promise<void> {
@@ -223,10 +225,14 @@ export class ReplServerClient {
223225

224226
const transport = await rpc.createClientPipeTransport(pipeName, "utf-8");
225227

228+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION, folder);
229+
const profiles = config.get<string[]>("profiles", []);
230+
226231
const { pythonCommand, final_args } = await this.pythonManager.buildRobotCodeCommand(
227232
folder,
228233
//["-v", "--debugpy", "--debugpy-wait-for-client", "repl-server", "--pipe", pipeName],
229234
["repl-server", "--pipe", pipeName, "--source", this.document.uri.fsPath],
235+
profiles,
230236
undefined,
231237
true,
232238
true,
@@ -279,16 +285,16 @@ export class ReplServerClient {
279285
this.connection = connection;
280286
}
281287

282-
async executeCell(source: string): Promise<{ success?: boolean; output: vscode.NotebookCellOutput }> {
283-
this._cancelationTokenSource = new vscode.CancellationTokenSource();
284-
288+
async executeCell(cell: vscode.NotebookCell): Promise<{ success?: boolean; output: vscode.NotebookCellOutput }> {
289+
const _cancelationTokenSource = new vscode.CancellationTokenSource();
290+
this._cancelationTokenSources.set(_cancelationTokenSource, cell);
285291
try {
286292
await this.ensureInitialized();
287293

288294
const result = await this.connection?.sendRequest<ExecutionResult>(
289295
"executeCell",
290-
{ source },
291-
this._cancelationTokenSource.token,
296+
{ source: cell.document.getText(), language_id: cell.document.languageId },
297+
_cancelationTokenSource.token,
292298
);
293299

294300
return {
@@ -306,10 +312,14 @@ export class ReplServerClient {
306312
),
307313
};
308314
} finally {
309-
this._cancelationTokenSource.dispose();
310-
this._cancelationTokenSource = undefined;
315+
this._cancelationTokenSources.delete(_cancelationTokenSource);
316+
_cancelationTokenSource.dispose();
311317
}
312318
}
319+
320+
async interrupt(): Promise<void> {
321+
await this.connection?.sendRequest("interrupt");
322+
}
313323
}
314324

315325
export class REPLNotebookController {
@@ -320,7 +330,7 @@ export class REPLNotebookController {
320330
readonly description = "A Robot Framework REPL notebook controller";
321331
readonly supportsExecutionOrder = true;
322332
readonly controller: vscode.NotebookController;
323-
readonly _clients = new Map<vscode.NotebookDocument, ReplServerClient>();
333+
readonly clients = new Map<vscode.NotebookDocument, ReplServerClient>();
324334

325335
_outputChannel: vscode.OutputChannel | undefined;
326336

@@ -343,18 +353,23 @@ export class REPLNotebookController {
343353
this.controller.supportsExecutionOrder = true;
344354
this.controller.description = "Robot Framework REPL";
345355
this.controller.interruptHandler = async (notebook: vscode.NotebookDocument) => {
346-
this._clients.get(notebook)?.dispose();
347-
this._clients.delete(notebook);
356+
this.clients.get(notebook)?.interrupt();
348357
};
349358
this._disposables = vscode.Disposable.from(
350359
this.controller,
351360
vscode.workspace.onDidCloseNotebookDocument((document) => {
352-
this._clients.get(document)?.dispose();
353-
this._clients.delete(document);
361+
this.disposeDocument(document);
354362
}),
355363
);
356364
}
357365

366+
disposeDocument(notebook: vscode.NotebookDocument): void {
367+
const client = this.clients.get(notebook);
368+
client?.interrupt();
369+
client?.dispose();
370+
this.clients.delete(notebook);
371+
}
372+
358373
outputChannel(): vscode.OutputChannel {
359374
if (!this._outputChannel) {
360375
this._outputChannel = vscode.window.createOutputChannel("RobotCode REPL");
@@ -363,18 +378,18 @@ export class REPLNotebookController {
363378
}
364379

365380
dispose(): void {
366-
for (const client of this._clients.values()) {
381+
for (const client of this.clients.values()) {
367382
client.dispose();
368383
}
369384
this._disposables.dispose();
370385
}
371386

372387
private getClient(document: vscode.NotebookDocument): ReplServerClient {
373-
let client = this._clients.get(document);
388+
let client = this.clients.get(document);
374389
if (!client) {
375390
client = new ReplServerClient(document, this.extensionContext, this.pythonManager, this.outputChannel());
376391
this.finalizeRegistry.register(document, client);
377-
this._clients.set(document, client);
392+
this.clients.set(document, client);
378393
}
379394
return client;
380395
}
@@ -398,8 +413,7 @@ export class REPLNotebookController {
398413

399414
execution.start(Date.now());
400415
try {
401-
const source = cell.document.getText();
402-
const result = await client.executeCell(source);
416+
const result = await client.executeCell(cell);
403417
if (result !== undefined) {
404418
success = result.success;
405419

@@ -445,6 +459,20 @@ export class NotebookManager {
445459
);
446460
await vscode.commands.executeCommand("vscode.openWith", newNotebook.uri, "robotframework-repl");
447461
}),
462+
vscode.commands.registerCommand("robotcode.notebookEditor.restartKernel", () => {
463+
const notebook = vscode.window.activeNotebookEditor?.notebook;
464+
if (notebook) {
465+
vscode.window.withProgress(
466+
{
467+
location: vscode.ProgressLocation.Notification,
468+
title: "Restarting kernel...",
469+
},
470+
async (_progress, _token) => {
471+
this._notebookController.disposeDocument(notebook);
472+
},
473+
);
474+
}
475+
}),
448476
);
449477
}
450478

0 commit comments

Comments
 (0)