Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shut down the Tribler Core process if the GUI process crashes. #6995

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import logging
import os
from typing import Callable, Optional

import psutil
from ipv8.taskmanager import TaskManager


GUI_PID_ENV_KEY = 'TRIBLER_GUI_PID'
CHECK_INTERVAL = 10


logger = logging.getLogger(__name__)


class GuiProcessNotRunning(Exception):
pass


class GuiProcessWatcher(TaskManager):

def __init__(self, gui_process: psutil.Process, shutdown_callback: Callable[[], None]):
super().__init__()
self.gui_process = gui_process
self.shutdown_callback = shutdown_callback
self.shutdown_callback_called = False

def start(self):
self.register_task("check GUI process", self.check_gui_process, interval=CHECK_INTERVAL)

async def stop(self):
await self.shutdown_task_manager()

def check_gui_process(self):
if self.shutdown_callback_called:
logger.info('The shutdown callback was already called; skip checking the GUI process')
return

p = self.gui_process
if p.is_running() and p.status() != psutil.STATUS_ZOMBIE:
logger.info('GUI process checked, it is still working')
else:
logger.info('GUI process is not working, initiate Core shutdown')
self.shutdown_callback_called = True
self.shutdown_callback()

@staticmethod
def get_gui_pid() -> Optional[int]:
pid = os.environ.get(GUI_PID_ENV_KEY, None)
if pid:
try:
return int(pid)
except ValueError:
logger.warning(f'Cannot parse {GUI_PID_ENV_KEY} environment variable: {pid}')
return None

@classmethod
def get_gui_process(cls) -> Optional[psutil.Process]:
pid = cls.get_gui_pid()
try:
return psutil.Process(pid) if pid else None
except psutil.NoSuchProcess as e:
raise GuiProcessNotRunning('The specified GUI process is not running. Is it already crashed?') from e
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from tribler.core.components.component import Component
from tribler.core.components.gui_process_watcher.gui_process_watcher import GuiProcessWatcher


class GuiProcessWatcherComponent(Component):
watcher: GuiProcessWatcher = None

async def run(self):
await super().run()

gui_process = GuiProcessWatcher.get_gui_process()
if not gui_process:
self.logger.warning('Cannot found GUI process to watch')
return

self.watcher = GuiProcessWatcher(gui_process, self.session.shutdown_event.set)
self.logger.info(f'Watching GUI process with pid {self.watcher.gui_process.pid}')
self.watcher.start()

async def shutdown(self):
await super().shutdown()
if self.watcher:
await self.watcher.stop()
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import os
from unittest.mock import Mock, patch

import psutil
import pytest

from tribler.core.components.gui_process_watcher.gui_process_watcher import GuiProcessNotRunning, GuiProcessWatcher, \
GUI_PID_ENV_KEY


def test_get_gui_pid(caplog):
with patch.dict(os.environ, {GUI_PID_ENV_KEY: ''}):
assert GuiProcessWatcher.get_gui_pid() is None

with patch.dict(os.environ, {GUI_PID_ENV_KEY: 'abc'}):
caplog.clear()
assert GuiProcessWatcher.get_gui_pid() is None
assert caplog.records[-1].message == 'Cannot parse TRIBLER_GUI_PID environment variable: abc'

with patch.dict(os.environ, {GUI_PID_ENV_KEY: '123'}):
assert GuiProcessWatcher.get_gui_pid() == 123


def test_get_gui_process():
# pid is not specified
with patch.dict(os.environ, {GUI_PID_ENV_KEY: ''}):
assert GuiProcessWatcher.get_gui_process() is None

pid = os.getpid()
with patch.dict(os.environ, {GUI_PID_ENV_KEY: str(pid)}):
# Process with the specified pid exists
p = GuiProcessWatcher.get_gui_process()
assert isinstance(p, psutil.Process)
assert p.pid == pid

# Process with the specified pid does not exist
exception = psutil.NoSuchProcess(pid, name='name', msg='msg')
with patch('psutil.Process', side_effect=exception):
with pytest.raises(GuiProcessNotRunning):
GuiProcessWatcher.get_gui_process()


def test_check_gui_process(caplog):
gui_process = Mock()
gui_process.is_running.return_value = True
gui_process.status.return_value = psutil.STATUS_RUNNING

# GUI process is working
shutdown_callback = Mock()
watcher = GuiProcessWatcher(gui_process, shutdown_callback)
caplog.clear()
watcher.check_gui_process()
assert caplog.records[-1].message == 'GUI process checked, it is still working'
assert not shutdown_callback.called
assert not watcher.shutdown_callback_called

# GUI process is zombie
gui_process.status.return_value = psutil.STATUS_ZOMBIE
watcher = GuiProcessWatcher(gui_process, shutdown_callback)
caplog.clear()
watcher.check_gui_process()
assert caplog.records[-1].message == 'GUI process is not working, initiate Core shutdown'
assert shutdown_callback.called
assert watcher.shutdown_callback_called

# The process is not running
gui_process.is_running.return_value = False
gui_process.status.reset_mock()
watcher = GuiProcessWatcher(gui_process, shutdown_callback)
caplog.clear()
watcher.check_gui_process()
assert not gui_process.status.called
assert caplog.records[-1].message == 'GUI process is not working, initiate Core shutdown'
assert shutdown_callback.called

# Calling check_gui_process after shutdown_callback was already called
shutdown_callback.reset_mock()
caplog.clear()
watcher.check_gui_process()
assert caplog.records[-1].message == 'The shutdown callback was already called; skip checking the GUI process'
assert watcher.shutdown_callback_called
assert not shutdown_callback.called
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os
from unittest.mock import patch

from tribler.core.components.gui_process_watcher.gui_process_watcher import GUI_PID_ENV_KEY
from tribler.core.components.gui_process_watcher.gui_process_watcher_component import GuiProcessWatcherComponent
from tribler.core.components.session import Session


async def test_watch_folder_component(tribler_config):
with patch.dict(os.environ, {GUI_PID_ENV_KEY: str(os.getpid())}):
components = [GuiProcessWatcherComponent()]
async with Session(tribler_config, components) as session:
comp = session.get_instance(GuiProcessWatcherComponent)
assert comp.started_event.is_set() and not comp.failed
assert comp.watcher
2 changes: 2 additions & 0 deletions src/tribler/core/start_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from tribler.core.components.component import Component
from tribler.core.components.gigachannel.gigachannel_component import GigaChannelComponent
from tribler.core.components.gigachannel_manager.gigachannel_manager_component import GigachannelManagerComponent
from tribler.core.components.gui_process_watcher.gui_process_watcher_component import GuiProcessWatcherComponent
from tribler.core.components.ipv8.ipv8_component import Ipv8Component
from tribler.core.components.key.key_component import KeyComponent
from tribler.core.components.libtorrent.libtorrent_component import LibtorrentComponent
Expand Down Expand Up @@ -49,6 +50,7 @@ def components_gen(config: TriblerConfig):
"""This function defines components that will be used in Tibler
"""
yield ReporterComponent()
yield GuiProcessWatcherComponent()
if config.api.http_enabled or config.api.https_enabled:
yield RESTComponent()
if config.chant.enabled or config.torrent_checking.enabled:
Expand Down
1 change: 1 addition & 0 deletions src/tribler/gui/core_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def start_tribler_core(self):
core_env.insert("CORE_API_PORT", f"{self.api_port}")
core_env.insert("CORE_API_KEY", self.api_key)
core_env.insert("TSTATEDIR", str(self.root_state_dir))
core_env.insert("TRIBLER_GUI_PID", str(os.getpid()))

core_args = self.core_args
if not core_args:
Expand Down