From e76e76bbe475d7f20a47e018384b55e8df53dc8d Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Sat, 13 Mar 2021 12:44:23 +0000 Subject: [PATCH 1/3] Add MacOS pyobjc to requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 7f6952ba..a7079592 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,6 @@ watchdog==0.10.3 # argh==0.26.2 watchdog dep # pyyaml==5.3.1 watchdog dep semantic-version==2.8.5 + +# Base requirement for MacOS +pyobjc; sys_platform == 'darwin' From 60c67723cae52e3564d99dda7df7b0f0be57cffe Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Fri, 12 Mar 2021 19:30:34 +0000 Subject: [PATCH 2/3] Change JournalLock.obtain_lock() return to an Enum. This way we can tell the difference between: 1. This process obtained the lock. 2. Another process has the lock. 3. We couldn't get the lock due to not being able to open the lock file read-write. Case 3 is currently also returned if the configured journal directory doesn't exist. This will be the case on any MacOS system that never had the game running. Likely given the OS hasn't been supported for the game in years now. --- EDMarketConnector.py | 10 ++++------ journal_lock.py | 24 +++++++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index bcc64c80..764568ec 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -33,7 +33,7 @@ if __name__ == '__main__': # After the redirect in case config does logging setup from config import appversion, appversion_nobuild, config, copyright from EDMCLogging import edmclogger, logger, logging -from journal_lock import JournalLock +from journal_lock import JournalLock, JournalLockResult if __name__ == '__main__': # noqa: C901 # Command-line arguments @@ -72,16 +72,14 @@ if __name__ == '__main__': # noqa: C901 config.set_auth_force_localserver() def handle_edmc_callback_or_foregrounding(): # noqa: CCR001 - """ - Handle any edmc:// auth callback, else foreground existing window. - """ + """Handle any edmc:// auth callback, else foreground existing window.""" logger.trace('Begin...') if platform == 'win32': # If *this* instance hasn't locked, then another already has and we # now need to do the edmc:// checks for auth callback - if not locked: + if locked != JournalLockResult.LOCKED: import ctypes from ctypes.wintypes import BOOL, HWND, INT, LPARAM, LPCWSTR, LPWSTR @@ -190,7 +188,7 @@ if __name__ == '__main__': # noqa: C901 handle_edmc_callback_or_foregrounding() - if not locked: + if locked == JournalLockResult.ALREADY_LOCKED: # There's a copy already running. logger.info("An EDMarketConnector.exe process was already running, exiting.") diff --git a/journal_lock.py b/journal_lock.py index 33279c76..8dab6ddc 100644 --- a/journal_lock.py +++ b/journal_lock.py @@ -2,6 +2,7 @@ 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 @@ -17,6 +18,15 @@ 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 + + class JournalLock: """Handle locking of journal directory.""" @@ -28,11 +38,11 @@ class JournalLock: # 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 - def obtain_lock(self) -> bool: + def obtain_lock(self) -> JournalLockResult: """ Attempt to obtain a lock on the journal directory. - :return: bool - True if we successfully obtained the lock + :return: LockResult - See the class Enum definition """ 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}') @@ -44,7 +54,7 @@ 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 False + return JournalLockResult.JOURNALDIR_READONLY if platform == 'win32': logger.trace('win32, using msvcrt') @@ -57,7 +67,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 False + return JournalLockResult.ALREADY_LOCKED else: logger.trace('NOT win32, using fcntl') @@ -67,7 +77,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 True # Lie about being locked + return JournalLockResult.LOCKED try: fcntl.flock(self.journal_dir_lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) @@ -75,13 +85,13 @@ 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 False + return JournalLockResult.ALREADY_LOCKED self.journal_dir_lockfile.write(f"Path: {self.journal_dir}\nPID: {os_getpid()}\n") self.journal_dir_lockfile.flush() logger.trace('Done') - return True + return JournalLockResult.LOCKED def release_lock(self) -> bool: """ From 1cd0392527d4975171540628e98d3f4e346eba63 Mon Sep 17 00:00:00 2001 From: Athanasius <github@miggy.org> Date: Sat, 13 Mar 2021 14:49:21 +0000 Subject: [PATCH 3/3] Record if JournalLock.obtain_lock() succeeded, use in release_lock() In the scenario of the default Journals location not even existing the user needs might update it to a network mount. When they do so we attempt, and fail, to unlock the old location, despite it not being locked. So now we have this boolean so we know if we should even attempt unlocking. --- journal_lock.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/journal_lock.py b/journal_lock.py index 8dab6ddc..e947ffe1 100644 --- a/journal_lock.py +++ b/journal_lock.py @@ -37,6 +37,7 @@ class JournalLock: 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 def obtain_lock(self) -> JournalLockResult: """ @@ -91,6 +92,8 @@ class JournalLock: self.journal_dir_lockfile.flush() logger.trace('Done') + self.locked = True + return JournalLockResult.LOCKED def release_lock(self) -> bool: @@ -99,6 +102,9 @@ class JournalLock: :return: bool - Success of unlocking operation. """ + if not self.locked: + return True # We weren't locked, and still aren't + unlocked = False if platform == 'win32': logger.trace('win32, using msvcrt')