Skip to content

Commit

Permalink
Enable killing of persistent console processes
Browse files Browse the repository at this point in the history
Added an action to the console context menu that kills the console process.
A killed console refuses to take any input but can be restored by
restarting.

Re #1586
  • Loading branch information
soininen committed Mar 15, 2023
1 parent c27ea0f commit 34c52ee
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
in Database editor.
- Scenarios can now be copied and pasted between databases in Database editor.
- Scenarios can now be duplicated in Database editor.
- Tool project item's Python and Julia consoles can now be killed from the right-click context menu.
A killed console can be restored by restarting it.
- A new option "Kill consoles at the end of execution" has been added to Tool properties
that kills Python and Julia console processes after execution saving some memory and computing resources.

### Changed
- The console settings of Python tools as well as the command and shell settings of executable tools
Expand Down
4 changes: 4 additions & 0 deletions spinetoolbox/spine_engine_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ def _handle_persistent_execution_msg(self, msg):
"Please go to Settings->Tools and check your setup."
)
self._event_message_arrived.emit(item, msg["filter_id"], "msg_error", msg_text)
elif msg_type == "persistent_killed":
self._logger.persistent_killed(item, msg["filter_id"])
elif msg_type == "stdin":
self._logger.add_persistent_stdin(item, msg["filter_id"], msg["data"])
elif msg_type == "stdout":
Expand All @@ -299,6 +301,8 @@ def _handle_persistent_execution_msg(self, msg):
item, msg["filter_id"], "msg", f"*** Starting execution on persistent process <b>{msg['args']}</b> ***"
)
self._event_message_arrived.emit(item, msg["filter_id"], "msg_warning", "See Console for messages")
else:
raise RuntimeError(f"Logic error: unknown persistent execution msg_type '{msg_type}'")

def _handle_kernel_execution_msg(self, msg):
item = self._project_items[msg["item_name"]] or self._connections.get(msg["item_name"])
Expand Down
3 changes: 3 additions & 0 deletions spinetoolbox/ui_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2426,6 +2426,9 @@ def _setup_persistent_console(self, item, filter_id, key, language):
d[filter_id] = self._make_persistent_console(item, key, language)
self.override_console_and_execution_list()

def persistent_killed(self, item, filter_id):
self._get_console(item, filter_id).set_killed(True)

def add_persistent_stdin(self, item, filter_id, data):
self._get_console(item, filter_id).add_stdin(data)

Expand Down
27 changes: 24 additions & 3 deletions spinetoolbox/widgets/persistent_console_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class PersistentConsoleWidget(QPlainTextEdit):
_history_item_available = Signal(str, str)
_completions_available = Signal(str, str, list)
_restarted = Signal()
_killed = Signal(bool)
_flush_needed = Signal()
_FLUSH_INTERVAL = 200
_MAX_LINES_PER_SECOND = 2000
Expand Down Expand Up @@ -232,6 +233,7 @@ def __init__(self, toolbox, key, language, owner=None):
self._history_item_available.connect(self._display_history_item)
self._completions_available.connect(self._display_completions)
self._restarted.connect(self._handle_restarted)
self._killed.connect(self._do_set_killed)

def closeEvent(self, ev):
super().closeEvent(ev)
Expand Down Expand Up @@ -470,6 +472,26 @@ def _insert_text(self, cursor, text, with_prompt):
else:
self._insert_stdout_text(cursor, text)

def set_killed(self, killed):
"""Emits the ``killed`` signal.
Args:
killed (bool): if True, may the console rest in peace
"""
self._killed.emit(killed)

@Slot(bool)
def _do_set_killed(self, killed):
"""Sets the console as killed or alive.
Args:
killed (bool): if True, may the console rest in peace
"""
self._is_dead = killed
self._line_edit.setVisible(not killed)
if killed:
self._make_prompt_block("Console killed (can be restarted from the right-click context menu)")

def add_stdin(self, data):
"""Adds new prompt with data. Used when adding stdin from external execution.
Expand Down Expand Up @@ -696,8 +718,8 @@ def _do_restart_persistent(self):

@Slot()
def _handle_restarted(self):
self._do_set_killed(False)
self._make_prompt_block(prompt=self._prompt)
self._is_dead = False

@Slot(bool)
def _interrupt_persistent(self, _=False):
Expand All @@ -714,9 +736,8 @@ def _do_interrupt_persistent(self):
@Slot(bool)
def _kill_persistent(self, _=False):
"""Sends a task to executor which will kill the underlying persistent process."""
self._do_set_killed(True)
self._executor.submit(self._do_kill_persistent)
self.add_stdout("Killed.")
self._is_dead = True

def _do_kill_persistent(self):
"""Kills underlying persistent process."""
Expand Down
8 changes: 6 additions & 2 deletions tests/server/test_EngineClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ def test_engine_client_send_issue_persistent_command(self):
client = EngineClient("localhost", 5601, ClientSecurityModel.NONE, "")
logger = mock.MagicMock()
logger.msg_warning = mock.MagicMock()
exec_mngr1 = PythonPersistentExecutionManager(logger, ["python"], [], "alias", group_id="SomeGroup")
exec_mngr1 = PythonPersistentExecutionManager(
logger, ["python"], [], "alias", kill_completed_processes=False, group_id="SomeGroup"
)
# Make exec_mngr live on the server, then send command from client to server for processing and check output
self.service.persistent_exec_mngrs["123"] = exec_mngr1
gener = client.send_issue_persistent_command("123", "print('hi')")
Expand All @@ -144,7 +146,9 @@ def test_engine_client_send_is_complete(self):
client = EngineClient("localhost", 5601, ClientSecurityModel.NONE, "")
logger = mock.MagicMock()
logger.msg_warning = mock.MagicMock()
exec_mngr1 = PythonPersistentExecutionManager(logger, ["python"], [], "alias", group_id="SomeGroup")
exec_mngr1 = PythonPersistentExecutionManager(
logger, ["python"], [], "alias", kill_completed_processes=False, group_id="SomeGroup"
)
self.service.persistent_exec_mngrs["123"] = exec_mngr1
should_be_true = client.send_is_complete("123", "print('hi')")
self.assertTrue(should_be_true)
Expand Down

0 comments on commit 34c52ee

Please sign in to comment.