diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 8caadca9..ae3b6d69 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -30,7 +30,7 @@ import asyncio from gns3server.utils import parse_version from gns3server.utils.telnet_server import TelnetServer -from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation +from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation, locked_coroutine from .virtualbox_error import VirtualBoxError from ..nios.nio_udp import NIOUDP from ..nios.nio_nat import NIONAT @@ -230,7 +230,7 @@ class VirtualBoxVM(BaseVM): if (yield from self.check_hw_virtualization()): self._hw_virtualization = True - @asyncio.coroutine + @locked_coroutine def stop(self): """ Stops this VirtualBox VM. diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index c7dcc880..068a7514 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -127,3 +127,24 @@ def wait_for_named_pipe_creation(pipe_path, timeout=60): else: return raise asyncio.TimeoutError() + + +def locked_coroutine(f): + """ + Method decorator that replace asyncio.coroutine that warranty + that this specific method of this class instance will not we + executed twice at the same time + """ + @asyncio.coroutine + def new_function(*args, **kwargs): + + # In the instance of the class we will store + # a lock has an attribute. + lock_var_name = "__" + f.__name__ + "_lock" + if not hasattr(args[0], lock_var_name): + setattr(args[0], lock_var_name, asyncio.Lock()) + + with (yield from getattr(args[0], lock_var_name)): + return (yield from f(*args, **kwargs)) + + return new_function diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py index b4fc7ae6..0b692c8d 100644 --- a/tests/utils/test_asyncio.py +++ b/tests/utils/test_asyncio.py @@ -21,7 +21,7 @@ import pytest import sys from unittest.mock import MagicMock -from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, wait_for_process_termination +from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, wait_for_process_termination, locked_coroutine def test_wait_run_in_executor(loop): @@ -67,3 +67,26 @@ def test_wait_for_process_termination(loop): exec = wait_for_process_termination(process, timeout=0.5) with pytest.raises(asyncio.TimeoutError): loop.run_until_complete(asyncio.async(exec)) + + +def test_lock_decorator(loop): + """ + The test check if the the second call to method_to_lock wait for the + first call to finish + """ + + class TestLock: + def __init__(self): + self._test_val = 0 + + @locked_coroutine + def method_to_lock(self): + res = self._test_val + yield from asyncio.sleep(0.1) + self._test_val += 1 + return res + + i = TestLock() + res = set(loop.run_until_complete(asyncio.gather(i.method_to_lock(), i.method_to_lock()))) + assert res == set((0, 1,)) # We use a set to test this to avoid order issue +