1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-13 07:47:14 +03:00

Move JournalLock class into its own file

Closes #891

# Conflicts:
#	journal_lock.py
#	monitor.py
This commit is contained in:
Athanasius 2021-03-22 14:30:41 +00:00
parent ceb2f74855
commit d39c0521da
3 changed files with 29 additions and 281 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)