From d39c0521da6f3650a87b3b10b4a54c5377f41a3a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 22 Mar 2021 14:30:41 +0000 Subject: [PATCH] Move JournalLock class into its own file Closes #891 # Conflicts: # journal_lock.py # monitor.py --- EDMarketConnector.py | 2 +- journal_lock.py | 111 ++++++------------------ monitor.py | 197 +------------------------------------------ 3 files changed, 29 insertions(+), 281 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index bdc2145b..0ba68077 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -39,7 +39,7 @@ from config import appversion, appversion_nobuild, config, copyright # isort: on from EDMCLogging import edmclogger, logger, logging -from monitor import JournalLock +from journal_lock import JournalLock if __name__ == '__main__': # noqa: C901 # Command-line arguments diff --git a/journal_lock.py b/journal_lock.py index 51f94f89..e769b31d 100644 --- a/journal_lock.py +++ b/journal_lock.py @@ -1,12 +1,9 @@ -"""Implements locking of Journal directory.""" - import pathlib import tkinter as tk -from enum import Enum from os import getpid as os_getpid from sys import platform from tkinter import ttk -from typing import TYPE_CHECKING, Callable, Optional +from typing import Callable, TYPE_CHECKING from config import config from EDMCLogging import get_main_logger @@ -18,48 +15,22 @@ if TYPE_CHECKING: return x -class JournalLockResult(Enum): - """Enumeration of possible outcomes of trying to lock the Journal Directory.""" - - LOCKED = 1 - JOURNALDIR_NOTEXIST = 2 - JOURNALDIR_READONLY = 3 - ALREADY_LOCKED = 4 - JOURNALDIR_IS_NONE = 5 - - class JournalLock: """Handle locking of journal directory.""" - def __init__(self) -> None: + def __init__(self): """Initialise where the journal directory and lock file are.""" - self.journal_dir: str = config.get('journaldir') or config.default_journal_dir - self.journal_dir_path: Optional[pathlib.Path] = None - self.set_path_from_journaldir() - self.journal_dir_lockfile_name: Optional[pathlib.Path] = None - # We never test truthiness of this, so let it be defined when first assigned. Avoids type hint issues. - # self.journal_dir_lockfile: Optional[IO] = None - self.locked = False + self.journal_dir: str = config.get_str('journaldir') or config.default_journal_dir + self.journal_dir_path = pathlib.Path(self.journal_dir) + self.journal_dir_lockfile_name = None + self.journal_dir_lockfile = None - def set_path_from_journaldir(self): - if self.journal_dir is None: - self.journal_dir_path = None - - else: - try: - self.journal_dir_path = pathlib.Path(self.journal_dir) - - except Exception: - logger.exception("Couldn't make pathlib.Path from journal_dir") - - def obtain_lock(self) -> JournalLockResult: + def obtain_lock(self) -> bool: """ Attempt to obtain a lock on the journal directory. - :return: LockResult - See the class Enum definition + :return: bool - True if we successfully obtained the lock """ - if self.journal_dir_path is None: - return JournalLockResult.JOURNALDIR_IS_NONE self.journal_dir_lockfile_name = self.journal_dir_path / 'edmc-journal-lock.txt' logger.trace(f'journal_dir_lockfile_name = {self.journal_dir_lockfile_name!r}') @@ -71,7 +42,6 @@ class JournalLock: except Exception as e: # For remote FS this could be any of a wide range of exceptions logger.warning(f"Couldn't open \"{self.journal_dir_lockfile_name}\" for \"w+\"" f" Aborting duplicate process checks: {e!r}") - return JournalLockResult.JOURNALDIR_READONLY if platform == 'win32': logger.trace('win32, using msvcrt') @@ -84,7 +54,7 @@ class JournalLock: except Exception as e: logger.info(f"Exception: Couldn't lock journal directory \"{self.journal_dir}\"" f", assuming another process running: {e!r}") - return JournalLockResult.ALREADY_LOCKED + return False else: logger.trace('NOT win32, using fcntl') @@ -94,7 +64,7 @@ class JournalLock: except ImportError: logger.warning("Not on win32 and we have no fcntl, can't use a file lock!" "Allowing multiple instances!") - return JournalLockResult.LOCKED + return True # Lie about being locked try: fcntl.flock(self.journal_dir_lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) @@ -102,25 +72,19 @@ class JournalLock: except Exception as e: logger.info(f"Exception: Couldn't lock journal directory \"{self.journal_dir}\", " f"assuming another process running: {e!r}") - return JournalLockResult.ALREADY_LOCKED + return False self.journal_dir_lockfile.write(f"Path: {self.journal_dir}\nPID: {os_getpid()}\n") self.journal_dir_lockfile.flush() logger.trace('Done') - self.locked = True - - return JournalLockResult.LOCKED + return True def release_lock(self) -> bool: """ Release lock on journal directory. - :return: bool - Whether we're now unlocked. - """ - if not self.locked: - return True # We weren't locked, and still aren't - + :return: bool - Success of unlocking operation.""" unlocked = False if platform == 'win32': logger.trace('win32, using msvcrt') @@ -161,21 +125,14 @@ class JournalLock: self.journal_dir_lockfile.close() self.journal_dir_lockfile_name = None - # Avoids type hint issues, see 'declaration' in JournalLock.__init__() - # self.journal_dir_lockfile = None + self.journal_dir_lockfile = None return unlocked class JournalAlreadyLocked(tk.Toplevel): """Pop-up for when Journal directory already locked.""" - def __init__(self, parent: tk.Tk, callback: Callable) -> None: - """ - Init the user choice popup. - - :param parent: - The tkinter parent window. - :param callback: - The function to be called when the user makes their choice. - """ + def __init__(self, parent: tk.Tk, callback: Callable): tk.Toplevel.__init__(self, parent) self.parent = parent @@ -207,30 +164,23 @@ class JournalLock: self.ignore_button.grid(row=2, column=1, sticky=tk.EW) self.protocol("WM_DELETE_WINDOW", self._destroy) - def retry(self) -> None: - """Handle user electing to Retry obtaining the lock.""" + def retry(self): logger.trace('User selected: Retry') self.destroy() self.callback(True, self.parent) - def ignore(self) -> None: - """Handle user electing to Ignore failure to obtain the lock.""" + def ignore(self): logger.trace('User selected: Ignore') self.destroy() self.callback(False, self.parent) - def _destroy(self) -> None: - """Destroy the Retry/Ignore popup.""" + def _destroy(self): logger.trace('User force-closed popup, treating as Ignore') self.ignore() - def update_lock(self, parent: tk.Tk) -> None: - """ - Update journal directory lock to new location if possible. - - :param parent: - The parent tkinter window. - """ - current_journaldir = config.get('journaldir') or config.default_journal_dir + def update_lock(self, parent: tk.Tk): + """Update journal directory lock to new location if possible.""" + current_journaldir = config.get_str('journaldir') or config.default_journal_dir if current_journaldir == self.journal_dir: return # Still the same @@ -238,27 +188,20 @@ class JournalLock: self.release_lock() self.journal_dir = current_journaldir - self.set_path_from_journaldir() - - if self.obtain_lock() == JournalLockResult.ALREADY_LOCKED: + self.journal_dir_path = pathlib.Path(self.journal_dir) + if not self.obtain_lock(): # Pop-up message asking for Retry or Ignore self.retry_popup = self.JournalAlreadyLocked(parent, self.retry_lock) - def retry_lock(self, retry: bool, parent: tk.Tk) -> None: - """ - Try again to obtain a lock on the Journal Directory. - - :param retry: - does the user want to retry? Comes from the dialogue choice. - :param parent: - The parent tkinter window. - """ + def retry_lock(self, retry: bool, parent: tk.Tk): logger.trace(f'We should retry: {retry}') if not retry: return - current_journaldir = config.get('journaldir') or config.default_journal_dir + current_journaldir = config.get_str('journaldir') or config.default_journal_dir self.journal_dir = current_journaldir - self.set_path_from_journaldir() - if self.obtain_lock() == JournalLockResult.ALREADY_LOCKED: + self.journal_dir_path = pathlib.Path(self.journal_dir) + if not self.obtain_lock(): # Pop-up message asking for Retry or Ignore self.retry_popup = self.JournalAlreadyLocked(parent, self.retry_lock) diff --git a/monitor.py b/monitor.py index 11caced1..abeae8de 100644 --- a/monitor.py +++ b/monitor.py @@ -2,16 +2,12 @@ from collections import defaultdict, OrderedDict import json import re import threading -from os import getpid as os_getpid from os import listdir, SEEK_SET, SEEK_END from os.path import basename, expanduser, isdir, join -import pathlib from sys import platform -import tkinter as tk -from tkinter import ttk from time import gmtime, localtime, sleep, strftime, strptime, time from calendar import timegm -from typing import Any, Callable, List, MutableMapping, Optional, OrderedDict as OrderedDictT, Tuple, TYPE_CHECKING, Union +from typing import Any, List, MutableMapping, Optional, OrderedDict as OrderedDictT, Tuple, TYPE_CHECKING, Union if TYPE_CHECKING: import tkinter @@ -1093,194 +1089,3 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below # singleton monitor = EDLogs() - - -class JournalLock: - """Handle locking of journal directory.""" - - def __init__(self): - """Initialise where the journal directory and lock file are.""" - self.journal_dir: str = config.get_str('journaldir') or config.default_journal_dir - self.journal_dir_path = pathlib.Path(self.journal_dir) - self.journal_dir_lock = None - self.journal_dir_lockfile_name = None - self.journal_dir_lockfile = None - - def obtain_lock(self) -> bool: - """ - Attempt to obtain a lock on the journal directory. - - :return: bool - True if we successfully obtained the lock - """ - logger.trace(f'journal_dir_lockfile = {self.journal_dir_lockfile!r}') - - self.journal_dir_lockfile_name: str = self.journal_dir_path / 'edmc-journal-lock.txt' - try: - self.journal_dir_lockfile = open(self.journal_dir_lockfile_name, mode='w+', encoding='utf-8') - - # Linux CIFS read-only mount throws: OSError(30, 'Read-only file system') - # Linux no-write-perm directory throws: PermissionError(13, 'Permission denied') - except Exception as e: # For remote FS this could be any of a wide range of exceptions - logger.warning(f"Couldn't open \"{self.journal_dir_lockfile_name}\" for \"w+\"" - f" Aborting duplicate process checks: {e!r}") - - if platform == 'win32': - logger.trace('win32, using msvcrt') - # win32 doesn't have fcntl, so we have to use msvcrt - import msvcrt - - try: - msvcrt.locking(self.journal_dir_lockfile.fileno(), msvcrt.LK_NBLCK, 4096) - - except Exception as e: - logger.info(f"Exception: Couldn't lock journal directory \"{self.journal_dir}\"" - f", assuming another process running: {e!r}") - return False - - else: - logger.trace('NOT win32, using fcntl') - try: - import fcntl - - except ImportError: - logger.warning("Not on win32 and we have no fcntl, can't use a file lock!" - "Allowing multiple instances!") - return True # Lie about being locked - - try: - fcntl.flock(self.journal_dir_lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) - - except Exception as e: - logger.info(f"Exception: Couldn't lock journal directory \"{self.journal_dir}\"," - f"assuming another process running: {e!r}") - return False - - self.journal_dir_lockfile.write(f"Path: {self.journal_dir}\nPID: {os_getpid()}\n") - self.journal_dir_lockfile.flush() - - logger.trace('Done') - return True - - def release_lock(self) -> bool: - """ - Release lock on journal directory. - - :return: bool - Success of unlocking operation.""" - unlocked = False - if platform == 'win32': - logger.trace('win32, using msvcrt') - # win32 doesn't have fcntl, so we have to use msvcrt - import msvcrt - - try: - msvcrt.locking(self.journal_dir_lockfile.fileno(), msvcrt.LK_UNLCK, 4096) - - except Exception as e: - logger.info(f"Exception: Couldn't unlock journal directory \"{self.journal_dir}\": {e!r}") - - else: - unlocked = True - - else: - logger.trace('NOT win32, using fcntl') - try: - import fcntl - - except ImportError: - logger.warning("Not on win32 and we have no fcntl, can't use a file lock!") - return True # Lie about being unlocked - - try: - fcntl.flock(self.journal_dir_lockfile, fcntl.LOCK_UN) - - except Exception as e: - logger.info(f"Exception: Couldn't unlock journal directory \"{self.journal_dir}\": {e!r}") - - else: - unlocked = True - - # Close the file whether or not the unlocking succeeded. - self.journal_dir_lockfile.close() - - self.journal_dir_lock = None - self.journal_dir_lockfile_name = None - self.journal_dir_lockfile = None - - return unlocked - - class JournalAlreadyLocked(tk.Toplevel): - """Pop-up for when Journal directory already locked.""" - - def __init__(self, parent: tk.Tk, callback: Callable): - tk.Toplevel.__init__(self, parent) - - self.parent = parent - self.callback = callback - self.title('Journal directory already locked') - - # remove decoration - if platform == 'win32': - self.attributes('-toolwindow', tk.TRUE) - - elif platform == 'darwin': - # http://wiki.tcl.tk/13428 - parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') - - self.resizable(tk.FALSE, tk.FALSE) - - frame = ttk.Frame(self) - frame.grid(sticky=tk.NSEW) - - self.blurb = tk.Label(frame) - self.blurb['text'] = '''The new Journal Directory location is already locked. -You can either attempt to resolve this and then Retry, or choose to Ignore this.''' - self.blurb.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW) - - self.retry_button = ttk.Button(frame, text='Retry', command=self.retry) - self.retry_button.grid(row=2, column=0, sticky=tk.EW) - - self.ignore_button = ttk.Button(frame, text='Ignore', command=self.ignore) - self.ignore_button.grid(row=2, column=1, sticky=tk.EW) - self.protocol("WM_DELETE_WINDOW", self._destroy) - - def retry(self): - logger.trace('User selected: Retry') - self.destroy() - self.callback(True, self.parent) - - def ignore(self): - logger.trace('User selected: Ignore') - self.destroy() - self.callback(False, self.parent) - - def _destroy(self): - logger.trace('User force-closed popup, treating as Ignore') - self.ignore() - - def update_lock(self, parent: tk.Tk): - """Update journal directory lock to new location if possible.""" - current_journaldir = config.get_str('journaldir') or config.default_journal_dir - - if current_journaldir == self.journal_dir: - return # Still the same - - self.release_lock() - - self.journal_dir = current_journaldir - self.journal_dir_path = pathlib.Path(self.journal_dir) - if not self.obtain_lock(): - # Pop-up message asking for Retry or Ignore - self.retry_popup = self.JournalAlreadyLocked(parent, self.retry_lock) - - def retry_lock(self, retry: bool, parent: tk.Tk): - logger.trace(f'We should retry: {retry}') - - if not retry: - return - - current_journaldir = config.get_str('journaldir') or config.default_journal_dir - self.journal_dir = current_journaldir - self.journal_dir_path = pathlib.Path(self.journal_dir) - if not self.obtain_lock(): - # Pop-up message asking for Retry or Ignore - self.retry_popup = self.JournalAlreadyLocked(parent, self.retry_lock)