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:
parent
ceb2f74855
commit
d39c0521da
@ -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
|
||||
|
111
journal_lock.py
111
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)
|
||||
|
197
monitor.py
197
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user