resource: Support waiting for reserved resources until available

Before this patch, almost everything was in place to support concurrent
osmo-gsm-tester instances sharing a common state dir. However, during
resource reservation, if the reservation couldn't be done due to too
many resources being in use, osmo-gsm-tester would fail and skip the
test suite.
With this patch, OGT will wait until some reserved resources are
released and then try requesting the reservation again.

Change-Id: I938602ee890712fda82fd3f812d8edb1bcd05e08
diff --git a/src/osmo_gsm_tester/core/util.py b/src/osmo_gsm_tester/core/util.py
index e035a72..691b489 100644
--- a/src/osmo_gsm_tester/core/util.py
+++ b/src/osmo_gsm_tester/core/util.py
@@ -28,6 +28,8 @@
 import threading
 import importlib.util
 import subprocess
+from watchdog.observers import Observer
+from watchdog.events import FileSystemEventHandler
 
 # This mirrors enum osmo_auth_algo in libosmocore/include/osmocom/crypt/auth.h
 # so that the index within the tuple matches the enum value.
@@ -302,6 +304,51 @@
     def __repr__(self):
         return self.path
 
+class FileWatch(FileSystemEventHandler):
+    def __init__(self, origin, watch_path, event_func):
+        FileSystemEventHandler.__init__(self)
+        self.origin = origin
+        self.watch_path = watch_path
+        self.event_func = event_func
+        self.observer = Observer()
+        self.watch = None
+        self.mutex = threading.Lock()
+
+    def get_lock(self):
+        return self.mutex
+
+    def start(self):
+        dir = os.path.abspath(os.path.dirname(self.watch_path))
+        self.origin.dbg('FileWatch: scheduling watch for directory %s' % dir)
+        self.watch = self.observer.schedule(self, dir, recursive = False)
+        self.observer.start()
+
+    def stop(self):
+        if self.watch:
+            self.origin.dbg('FileWatch: unscheduling watch %r' % self.watch)
+            self.observer.unschedule(self.watch)
+            self.watch = None
+        if self.observer.is_alive():
+            self.observer.stop()
+            self.observer.join()
+
+    def __del__(self):
+        self.stop()
+        self.observer = None
+
+    # Override from FileSystemEventHandler
+    def on_any_event(self, event):
+        if event.is_directory:
+            return None
+        if os.path.abspath(event.src_path) != os.path.abspath(self.watch_path):
+            return None
+        self.origin.dbg('FileWatch: received event %r' % event)
+        try:
+            self.mutex.acquire()
+            self.event_func(event)
+        finally:
+             self.mutex.release()
+
 def touch_file(path):
     with open(path, 'a') as f:
         f.close()