diff --git a/journal_lock.py b/journal_lock.py
new file mode 100644
index 00000000..48c3d572
--- /dev/null
+++ b/journal_lock.py
@@ -0,0 +1,231 @@
+"""Implements locking of Journal directory."""
+
+import pathlib
+import tkinter as tk
+from os import getpid as os_getpid
+from sys import platform
+from tkinter import ttk
+from typing import TYPE_CHECKING, Callable, Optional
+
+from config import config
+from EDMCLogging import get_main_logger
+
+logger = get_main_logger()
+
+if TYPE_CHECKING:
+    def _(x: str) -> str:
+        return x
+
+
+class JournalLock:
+    """Handle locking of journal directory."""
+
+    def __init__(self) -> None:
+        """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_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
+
+    def obtain_lock(self) -> bool:
+        """
+        Attempt to obtain a lock on the journal directory.
+
+        :return: bool - True if we successfully obtained the lock
+        """
+        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}')
+        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}")
+            return 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_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:
+                # Need to seek to the start first, as lock range is relative to
+                # current position
+                self.journal_dir_lockfile.seek(0)
+                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_lockfile_name = None
+        # Avoids type hint issues, see 'declaration' in JournalLock.__init__()
+        # 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.
+            """
+            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.{CR}"
+                                   "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) -> None:
+            """Handle user electing to Retry obtaining the lock."""
+            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."""
+            logger.trace('User selected: Ignore')
+            self.destroy()
+            self.callback(False, self.parent)
+
+        def _destroy(self) -> None:
+            """Destroy the Retry/Ignore popup."""
+            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_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) -> 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.
+        """
+        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)