Skip to content

Commit

Permalink
Add GuiProcessWatcherComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
kozlovsky committed Aug 12, 2022
1 parent 9f44f6a commit 7cfa04f
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 0 deletions.
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

0 comments on commit 7cfa04f

Please sign in to comment.