diff --git a/gns3server/compute/dynamips/__init__.py b/gns3server/compute/dynamips/__init__.py
index 56676e5d..5ffc01c2 100644
--- a/gns3server/compute/dynamips/__init__.py
+++ b/gns3server/compute/dynamips/__init__.py
@@ -239,23 +239,6 @@ class Dynamips(BaseManager):
if device.project.id == project.id:
yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower()))
- @asyncio.coroutine
- def project_committed(self, project):
- """
- Called when a project has been committed.
-
- :param project: Project instance
- """
-
- # save the configs when the project is committed
- for node in self._nodes.copy().values():
- if node.project.id == project.id:
- try:
- yield from node.save_configs()
- except DynamipsError as e:
- log.warning(e)
- continue
-
@property
def dynamips_path(self):
"""
diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py
index e53f6fc6..f3d0bc80 100644
--- a/gns3server/compute/dynamips/nodes/router.py
+++ b/gns3server/compute/dynamips/nodes/router.py
@@ -35,6 +35,8 @@ from ...base_node import BaseNode
from ..dynamips_error import DynamipsError
from ..nios.nio_udp import NIOUDP
+
+from gns3server.utils.file_watcher import FileWatcher
from gns3server.utils.asyncio import wait_run_in_executor, monitor_process
from gns3server.utils.images import md5sum
@@ -92,6 +94,7 @@ class Router(BaseNode):
self._system_id = "FTX0945W0MY" # processor board ID in IOS
self._slots = []
self._ghost_flag = ghost_flag
+ self._memory_watcher = None
if not ghost_flag:
if not dynamips_id:
@@ -160,6 +163,12 @@ class Router(BaseNode):
return router_info
+ def _memory_changed(self, path):
+ """
+ Called when the NVRAM file has changed
+ """
+ asyncio.async(self.save_configs())
+
@property
def dynamips_id(self):
"""
@@ -248,6 +257,8 @@ class Router(BaseNode):
yield from self._hypervisor.send('vm start "{name}"'.format(name=self._name))
self.status = "started"
log.info('router "{name}" [{id}] has been started'.format(name=self._name, id=self._id))
+
+ self._memory_watcher = FileWatcher(self._memory_files(), self._memory_changed, strategy='hash', delay=30)
monitor_process(self._hypervisor.process, self._termination_callback)
@asyncio.coroutine
@@ -278,6 +289,9 @@ class Router(BaseNode):
log.warn("Could not stop {}: {}".format(self._name, e))
self.status = "stopped"
log.info('Router "{name}" [{id}] has been stopped'.format(name=self._name, id=self._id))
+ if self._memory_watcher:
+ self._memory_watcher.close()
+ self._memory_watcher = None
yield from self.save_configs()
@asyncio.coroutine
@@ -1599,3 +1613,10 @@ class Router(BaseNode):
yield from self._hypervisor.send('vm clean_delete "{}"'.format(self._name))
self._hypervisor.devices.remove(self)
log.info('Router "{name}" [{id}] has been deleted (including associated files)'.format(name=self._name, id=self._id))
+
+ def _memory_files(self):
+ project_dir = os.path.join(self.project.module_working_directory(self.manager.module_name.lower()))
+ return [
+ os.path.join(project_dir, "{}_i{}_rom".format(self.platform, self.dynamips_id)),
+ os.path.join(project_dir, "{}_i{}_nvram".format(self.platform, self.dynamips_id))
+ ]
diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py
index f5ea8937..ab510ef4 100644
--- a/gns3server/compute/iou/iou_vm.py
+++ b/gns3server/compute/iou/iou_vm.py
@@ -503,7 +503,7 @@ class IOUVM(BaseNode):
# check if there is enough RAM to run
self.check_available_ram(self.ram)
- self._nvram_watcher = FileWatcher(self._nvram_file(), self._nvram_changed)
+ self._nvram_watcher = FileWatcher(self._nvram_file(), self._nvram_changed, delay=10)
# created a environment variable pointing to the iourc file.
env = os.environ.copy()
diff --git a/gns3server/utils/file_watcher.py b/gns3server/utils/file_watcher.py
index 2de0f7c1..415c6eb0 100644
--- a/gns3server/utils/file_watcher.py
+++ b/gns3server/utils/file_watcher.py
@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import zlib
import asyncio
import os
@@ -22,20 +23,43 @@ import os
class FileWatcher:
"""
Watch for file change and call the callback when something happen
+
+ :param paths: A path or a list of file to watch
+ :param delay: Delay between file check (seconds)
+ :param strategy: File change strategy (mtime: modification time, hash: hash compute)
"""
- def __init__(self, path, callback, delay=1):
- if not isinstance(path, str):
- path = str(path)
- self._path = path
+ def __init__(self, paths, callback, delay=1, strategy='mtime'):
+ self._paths = []
+ if not isinstance(paths, list):
+ paths = [paths]
+ for path in paths:
+ if not isinstance(path, str):
+ path = str(path)
+ self._paths.append(path)
+
self._callback = callback
self._delay = delay
self._closed = False
+ self._strategy = strategy
- try:
- self._mtime = os.stat(path).st_mtime_ns
- except OSError:
- self._mtime = None
+ if self._strategy == 'mtime':
+ # Store modification time
+ self._mtime = {}
+ for path in self._paths:
+ try:
+ self._mtime[path] = os.stat(path).st_mtime_ns
+ except OSError:
+ self._mtime[path] = None
+ else:
+ # Store hash
+ self._hashed = {}
+ for path in self._paths:
+ try:
+ # Alder32 is a fast bu insecure hash algorithm
+ self._hashed[path] = zlib.adler32(open(path, 'rb').read())
+ except OSError:
+ self._hashed[path] = None
asyncio.get_event_loop().call_later(self._delay, self._check_config_file_change)
def __del__(self):
@@ -48,15 +72,26 @@ class FileWatcher:
if self._closed:
return
changed = False
- try:
- mtime = os.stat(self._path).st_mtime_ns
- if mtime != self._mtime:
- changed = True
- self._mtime = mtime
- except OSError:
- self._mtime = None
- if changed:
- self._callback(self._path)
+
+ for path in self._paths:
+ if self._strategy == 'mtime':
+ try:
+ mtime = os.stat(path).st_mtime_ns
+ if mtime != self._mtime[path]:
+ changed = True
+ self._mtime[path] = mtime
+ except OSError:
+ self._mtime[path] = None
+ else:
+ try:
+ hashc = zlib.adler32(open(path, 'rb').read())
+ if hashc != self._hashed[path]:
+ changed = True
+ self._hashed[path] = hashc
+ except OSError:
+ self._hashed[path] = None
+ if changed:
+ self._callback(path)
asyncio.get_event_loop().call_later(self._delay, self._check_config_file_change)
@property
diff --git a/tests/utils/test_file_watcher.py b/tests/utils/test_file_watcher.py
index 11939d5f..2f07315c 100644
--- a/tests/utils/test_file_watcher.py
+++ b/tests/utils/test_file_watcher.py
@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import pytest
import asyncio
from unittest.mock import MagicMock
@@ -22,11 +23,12 @@ from unittest.mock import MagicMock
from gns3server.utils.file_watcher import FileWatcher
-def test_file_watcher(async_run, tmpdir):
+@pytest.mark.parametrize("strategy", ['mtime', 'hash'])
+def test_file_watcher(async_run, tmpdir, strategy):
file = tmpdir / "test"
file.write("a")
callback = MagicMock()
- fw = FileWatcher(file, callback, delay=0.5)
+ fw = FileWatcher(file, callback, delay=0.5, strategy=strategy)
async_run(asyncio.sleep(1))
assert not callback.called
file.write("b")
@@ -34,12 +36,29 @@ def test_file_watcher(async_run, tmpdir):
callback.assert_called_with(str(file))
-def test_file_watcher_not_existing(async_run, tmpdir):
+@pytest.mark.parametrize("strategy", ['mtime', 'hash'])
+def test_file_watcher_not_existing(async_run, tmpdir, strategy):
file = tmpdir / "test"
callback = MagicMock()
- fw = FileWatcher(file, callback, delay=0.5)
+ fw = FileWatcher(file, callback, delay=0.5, strategy=strategy)
async_run(asyncio.sleep(1))
assert not callback.called
file.write("b")
async_run(asyncio.sleep(1.5))
callback.assert_called_with(str(file))
+
+
+@pytest.mark.parametrize("strategy", ['mtime', 'hash'])
+def test_file_watcher_list(async_run, tmpdir, strategy):
+ file = tmpdir / "test"
+ file.write("a")
+
+ file2 = tmpdir / "test2"
+
+ callback = MagicMock()
+ fw = FileWatcher([file, file2], callback, delay=0.5, strategy=strategy)
+ async_run(asyncio.sleep(1))
+ assert not callback.called
+ file2.write("b")
+ async_run(asyncio.sleep(1.5))
+ callback.assert_called_with(str(file2))