diff --git a/bin/CraftOS/OsUtilsBase.py b/bin/CraftOS/OsUtilsBase.py index 0b9ee3363..13b590f7b 100644 --- a/bin/CraftOS/OsUtilsBase.py +++ b/bin/CraftOS/OsUtilsBase.py @@ -1,70 +1,93 @@ import abc import os import platform import sys from CraftOS.OsDetection import OsDetection class OsUtilsBase(OsDetection, metaclass=abc.ABCMeta): @abc.abstractstaticmethod def rm(path, force=False): """ Removes a file""" pass @abc.abstractstaticmethod def rmDir(path, force=False): """ Removes a file""" pass @abc.abstractstaticmethod def getFileAttributes(path): """ Returns the attributes""" pass @abc.abstractstaticmethod def removeReadOnlyAttribute(path): """ Removes the readonly flag""" pass def setConsoleTitle(title): """ Set the console title """ return True @staticmethod def supportsSymlinks() -> bool: return True @staticmethod def toWindowsPath(path : str) -> str: if not path: return path path = os.path.normpath(path) return path.replace("/", "\\") @staticmethod def toUnixPath(path : str) -> str: if not path: return path path = os.path.normpath(path) return path.replace("\\", "/") @staticmethod def toMSysPath(path): if not path: return path path = OsUtilsBase.toUnixPath(path) drive, path = os.path.splitdrive(path) if drive: return f"/{drive[0].lower()}{path}" return path @staticmethod def toNativePath(path : str) -> str: """Return a native path""" pass @staticmethod def killProcess(name : str="*", prefix : str=None) -> bool: pass + + +class LockFileBase(OsDetection, metaclass=abc.ABCMeta): + def __init__(self, name): + self.name = name + self._locked = False + + @property + def isLocked(self): + return self._locked + + def lock(self): + pass + + def unlock(self): + pass + + def __enter__(self): + self.lock() + return self + + def __exit__(self, exc_type, exc_value, trback): + self.unlock() diff --git a/bin/CraftOS/osutils.py b/bin/CraftOS/osutils.py index 64efb5bff..51fab46ea 100644 --- a/bin/CraftOS/osutils.py +++ b/bin/CraftOS/osutils.py @@ -1,6 +1,8 @@ import os if os.name == 'nt': from CraftOS.win.osutils import OsUtils as OsUtils + from CraftOS.OsUtilsBase import LockFileBase as LockFile else: from CraftOS.unix.osutils import OsUtils as OsUtils + from CraftOS.unix.osutils import LockFile as LockFile diff --git a/bin/CraftOS/unix/osutils.py b/bin/CraftOS/unix/osutils.py index 26814814a..346c4b1d0 100644 --- a/bin/CraftOS/unix/osutils.py +++ b/bin/CraftOS/unix/osutils.py @@ -1,53 +1,80 @@ +import fcntl import os import shutil import sys +import time import CraftOS.OsUtilsBase from CraftCore import CraftCore class OsUtils(CraftOS.OsUtilsBase.OsUtilsBase): @staticmethod def rm(path, force=False): CraftCore.log.debug("deleting file %s" % path) try: os.remove(path) return True except OSError as e: CraftCore.log.warning("could not delete file %s: %s" % (path, e)) return False @staticmethod def rmDir(path, force=False): CraftCore.log.debug("deleting directory %s" % path) try: shutil.rmtree(path) return True except OSError: return OsUtils.rm(path, force) return False @staticmethod def isLink(path): return os.path.islink(path) @staticmethod def removeReadOnlyAttribute(path): return False @staticmethod def setConsoleTitle(title): sys.stdout.buffer.write(b"\x1b]0;") sys.stdout.buffer.write(bytes(title, "UTF-8")) sys.stdout.buffer.write(b"\x07") sys.stdout.flush() return True @staticmethod def toNativePath(path : str) -> str: return OsUtils.toUnixPath(path) @staticmethod def killProcess(name : str="*", prefix : str=None) -> bool: CraftCore.log.warning("killProcess is not implemented") return True + +class LockFile(CraftOS.OsUtilsBase.LockFileBase): + def __init__(self ,name): + super().__init__(name) + self.__lockFileName = f"/tmp/craftlock-{self.name}" + self.__lockFile = None + + + def lock(self): + while True: + try: + if not self.__lockFile: + self.__lockFile = open(self.__lockFileName, 'w') + fcntl.flock(self.__lockFile, fcntl.LOCK_EX | fcntl.LOCK_NB) + self._locked = True + break + except IOError: + CraftCore.log.info(f"{self.__lockFileName} is locked waiting") + time.sleep(10) + + + def unlock(self): + if self._locked: + fcntl.flock(self.__lockFile, fcntl.LOCK_UN) + self._locked = False diff --git a/bin/test/test_OsUtils.py b/bin/test/test_OsUtils.py index e90741181..520e56889 100755 --- a/bin/test/test_OsUtils.py +++ b/bin/test/test_OsUtils.py @@ -1,51 +1,71 @@ import os import subprocess import tempfile +import time +import threading import unittest import CraftTestBase import utils from CraftCore import CraftCore -from CraftOS.osutils import OsUtils +from CraftOS.osutils import OsUtils, LockFile +from CraftOS.OsDetection import OsDetection class OsUtilsTest(CraftTestBase.CraftTestBase): def test_rm(self): _, fileName = tempfile.mkstemp() OsUtils.rm(fileName) def test_rmDir(self): dirName = tempfile.mkdtemp() OsUtils.rmDir(dirName) def test_killProcess(self): # TODO: find a better test app than the cmd windows - with tempfile.TemporaryDirectory() as tmp1: - with tempfile.TemporaryDirectory() as tmp2: - test1 = os.path.join(tmp1, "craft_test.exe") - test2 = os.path.join(tmp2, "craft_test.exe") - cmd = CraftCore.cache.findApplication("cmd") - self.assertEqual(utils.copyFile(cmd, test1, linkOnly=False), True) - self.assertEqual(utils.copyFile(cmd, test2, linkOnly=False), True) - process = subprocess.Popen([test1,"/K"], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) - process2 = subprocess.Popen([test2,"/K"], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) - try: - self.assertEqual(process.poll(), None) - self.assertEqual(process2.poll(), None) - self.assertEqual(OsUtils.killProcess("craft_test", tmp2), True) - self.assertEqual(process.poll(), None) - #ensure that process 2 was killed - self.assertNotEquals(process2.poll(), None) - except subprocess.SubprocessError as e: - CraftCore.log.warning(e) - finally: - process.kill() - process2.kill() - process.wait() - process2.wait() + if OsDetection.isWin(): + with tempfile.TemporaryDirectory() as tmp1: + with tempfile.TemporaryDirectory() as tmp2: + test1 = os.path.join(tmp1, "craft_test.exe") + test2 = os.path.join(tmp2, "craft_test.exe") + cmd = CraftCore.cache.findApplication("cmd") + self.assertEqual(utils.copyFile(cmd, test1, linkOnly=False), True) + self.assertEqual(utils.copyFile(cmd, test2, linkOnly=False), True) + process = subprocess.Popen([test1,"/K"], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) + process2 = subprocess.Popen([test2,"/K"], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) + try: + self.assertEqual(process.poll(), None) + self.assertEqual(process2.poll(), None) + self.assertEqual(OsUtils.killProcess("craft_test", tmp2), True) + self.assertEqual(process.poll(), None) + #ensure that process 2 was killed + self.assertNotEquals(process2.poll(), None) + except subprocess.SubprocessError as e: + CraftCore.log.warning(e) + finally: + process.kill() + process2.kill() + process.wait() + process2.wait() + + def test_LockFile(self): + if OsDetection.isUnix(): + lock = LockFile("foo") + print("start") + lock.lock() + + def _delayedUnlock(): + print("unlock1") + lock.unlock() + threading.Timer(5, _delayedUnlock).start() + with LockFile("foo") as lock2: + print("locked lock2") + print("end") + + if __name__ == '__main__': unittest.main()