-
Notifications
You must be signed in to change notification settings - Fork 452
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
186 additions
and
0 deletions.
There are no files selected for viewing
63 changes: 63 additions & 0 deletions
63
src/tribler/core/components/gui_process_watcher/gui_process_watcher.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
23 changes: 23 additions & 0 deletions
23
src/tribler/core/components/gui_process_watcher/gui_process_watcher_component.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
82 changes: 82 additions & 0 deletions
82
src/tribler/core/components/gui_process_watcher/tests/test_gui_process_watcher.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
15 changes: 15 additions & 0 deletions
15
src/tribler/core/components/gui_process_watcher/tests/test_gui_process_watcher_component.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters