From af410e0b15168c2399822f05008303413e0d8ed5 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 22:09:37 +0100
Subject: [PATCH 1/4] Added functions to read and write project lockfile
---
nw/config.py | 6 ++-
nw/constants/constants.py | 1 +
nw/project/project.py | 91 ++++++++++++++++++++++++++++++++++++++-
3 files changed, 96 insertions(+), 2 deletions(-)
diff --git a/nw/config.py b/nw/config.py
index 3149edef..94c30f71 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -20,7 +20,7 @@ from os import path, mkdir, unlink, rename
from datetime import datetime
from PyQt5.Qt import PYQT_VERSION_STR
-from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths
+from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
from nw.constants import nwFiles, nwUnicode
from nw.common import splitVersionNumber
@@ -156,6 +156,10 @@ class Config:
else:
self.osUnknown = True
+ # Other System Info
+ self.hostName = QSysInfo.machineHostName()
+ self.kernelVer = QSysInfo.kernelVersion()
+
# Packages
self.hasEnchant = False
self.hasSymSpell = False
diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index 4cc7f040..b39830bc 100644
--- a/nw/constants/constants.py
+++ b/nw/constants/constants.py
@@ -23,6 +23,7 @@ class nwFiles():
APP_ICON = "novelWriter.svg"
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
+ PROJ_LOCK = "nwProject.lock"
SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
diff --git a/nw/project/project.py b/nw/project/project.py
index 887fe38c..afde1e02 100644
--- a/nw/project/project.py
+++ b/nw/project/project.py
@@ -40,6 +40,7 @@ class NWProject():
self.projOpened = None # The time stamp of when the project file was opened
self.projChanged = None # The project has unsaved changes
self.projAltered = None # The project has been altered this session
+ self.lockedBy = None # Data on which computer has the project open
# Debug
self.handleSeed = None
@@ -178,7 +179,7 @@ class NWProject():
return
- def openProject(self, fileName):
+ def openProject(self, fileName, overrideLock=False):
"""Open the project file provided, or if doesn't exist, assume
it is a folder, and look for the file within it. If successful,
parse the XML of the file and populate the project variables and
@@ -201,6 +202,21 @@ class NWProject():
if not self._checkFolder(self.projMeta):
return
+ if overrideLock:
+ self._clearLockFile()
+
+ lockStatus = self._readLockFile()
+ if len(lockStatus) > 0:
+ if lockStatus[0] == "ERROR":
+ logger.warning("Failed to check lock file")
+ else:
+ logger.error("Project is locked, so not opening")
+ self.lockedBy = lockStatus
+ self.clearProject()
+ return False
+ else:
+ logger.verbose("Project is not locked")
+
try:
projectMaintenance(self)
except Exception as E:
@@ -308,6 +324,7 @@ class NWProject():
self.setProjectChanged(False)
self.projOpened = time()
self.projAltered = False
+ self._writeLockFile()
return True
@@ -398,14 +415,19 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
self.mainConf.saveRecentCache()
+ self._writeLockFile()
self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False)
return True
def closeProject(self):
+ """Close the current project and clear all meta data.
+ """
self._appendSessionStats()
+ self._clearLockFile()
self.clearProject()
+ self.lockedBy = None
return True
##
@@ -645,6 +667,73 @@ class NWProject():
# Internal Functions
##
+ def _readLockFile(self):
+ """Reads the lock file in the project folder.
+ """
+
+ if self.projPath is None:
+ return ["ERROR"]
+
+ lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
+ if not path.isfile(lockFile):
+ return []
+
+ try:
+ with open(lockFile, mode="r", encoding="utf8") as inFile:
+ theData = inFile.read()
+ theLines = theData.splitlines()
+ if len(theLines) == 4:
+ return theLines
+ else:
+ return ["ERROR"]
+
+ except Exception as e:
+ logger.error("Failed to read project lockfile")
+ logger.error(str(e))
+ return ["ERROR"]
+
+ return ["ERROR"]
+
+ def _writeLockFile(self):
+ """Writes a lock file to the project folder.
+ """
+
+ if self.projPath is None:
+ return False
+
+ lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
+ try:
+ with open(lockFile, mode="w+", encoding="utf8") as outFile:
+ outFile.write("%s\n" % self.mainConf.hostName)
+ outFile.write("%s\n" % self.mainConf.osType)
+ outFile.write("%s\n" % self.mainConf.kernelVer)
+ outFile.write("%d\n" % time())
+
+ except Exception as e:
+ logger.error("Failed to write project lockfile")
+ logger.error(str(e))
+ return False
+
+ return True
+
+ def _clearLockFile(self):
+ """Remove the lock file, if it exists.
+ """
+ if self.projPath is None:
+ return False
+
+ lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
+ if path.isfile(lockFile):
+ try:
+ unlink(lockFile)
+ return True
+ except Exception as e:
+ logger.error("Failed to remove project lockfile")
+ logger.error(str(e))
+ return False
+
+ return None
+
def _checkFolder(self, thePath):
if not path.isdir(thePath):
try:
From 99034619638546048445d763cfe42efdc718c2a6 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 22:09:59 +0100
Subject: [PATCH 2/4] Added lockfile check and dialog box to main GUI
---
nw/guimain.py | 44 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 40 insertions(+), 4 deletions(-)
diff --git a/nw/guimain.py b/nw/guimain.py
index a9cd7edb..a34f8f28 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -15,6 +15,7 @@ import time
import nw
from os import path
+from datetime import datetime
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence
@@ -49,9 +50,9 @@ class GuiMain(QMainWindow):
self.hasProject = False
self.isZenMode = False
- logger.info("OS: %s" % (
- self.mainConf.osType)
- )
+ logger.info("OS: %s" % self.mainConf.osType)
+ logger.info("Kernel: %s" % self.mainConf.kernelVer)
+ logger.info("Host: %s" % self.mainConf.hostName)
logger.info("Qt5 Version: %s (%d)" % (
self.mainConf.verQtString, self.mainConf.verQtValue)
)
@@ -319,7 +320,42 @@ class GuiMain(QMainWindow):
# Try to open the project
if not self.theProject.openProject(projFile):
- return False
+ if self.theProject.lockedBy is not None:
+ if self.mainConf.showGUI:
+ try:
+ lockDetails = (
+ "
The project was locked by the computer "
+ "'%s' (%s %s), last active on %s"
+ ) % (
+ self.theProject.lockedBy[0],
+ self.theProject.lockedBy[1],
+ self.theProject.lockedBy[2],
+ datetime.fromtimestamp(
+ int(self.theProject.lockedBy[3])
+ ).strftime("%x %X")
+ )
+ except:
+ lockDetails = ""
+
+ msgBox = QMessageBox()
+ msgRes = msgBox.warning(
+ self, "Project Locked", (
+ "The project is already open by another instance of %s, and is "
+ "therefore locked. Override lock and continue anyway?
"
+ "Note: If the program or the computer previously crashed, the lock "
+ "can safely be overridden. If, however, another instance of %s has "
+ "the project open, overriding the lock may corrupt the project, and "
+ "is not recommended.%s"
+ ) % (nw.__package__, nw.__package__, lockDetails),
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No
+ )
+ if msgRes == QMessageBox.Yes:
+ if not self.theProject.openProject(projFile, overrideLock=True):
+ return False
+ else:
+ return False
+ else:
+ return False
# project is loaded
self.hasProject = True
From 8f1c2daddf992c21d12b2de3aba08e9ef32ac325 Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 22:10:09 +0100
Subject: [PATCH 3/4] Fixed tests
---
tests/reference/proj/2_nwProject.nwx | 10 +++++-----
tests/test_project.py | 17 +++++++++++++++++
2 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx
index b78c14a4..0a8e6b4d 100644
--- a/tests/reference/proj/2_nwProject.nwx
+++ b/tests/reference/proj/2_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
@@ -72,28 +72,28 @@
0
0
- -
+
-
Timeline
ROOT
TIMELINE
New
False
- -
+
-
Object
ROOT
OBJECT
New
False
- -
+
-
Custom1
ROOT
CUSTOM
New
False
- -
+
-
Custom2
ROOT
CUSTOM
diff --git a/tests/test_project.py b/tests/test_project.py
index c2f4078d..6723dcc7 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -29,6 +29,7 @@ def testProjectNew(nwTempProj,nwRef,nwTemp):
assert theProject.newProject()
assert theProject.setProjectPath(nwTempProj)
assert theProject.saveProject()
+ assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2])
@pytest.mark.project
@@ -41,9 +42,21 @@ def testProjectSave(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx")
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
assert theProject.saveProject()
+ assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2])
assert not theProject.projChanged
+@pytest.mark.project
+def testProjectOpenTwice(nwTempProj,nwRef):
+ projFile = path.join(nwTempProj,"nwProject.nwx")
+ refFile = path.join(nwRef,"proj","1_nwProject.nwx")
+ assert theProject.openProject(projFile)
+ assert not theProject.openProject(projFile)
+ assert theProject.openProject(projFile, overrideLock=True)
+ assert theProject.saveProject()
+ assert theProject.closeProject()
+ assert cmpFiles(projFile, refFile, [2])
+
@pytest.mark.project
def testProjectNewRoot(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx")
@@ -59,6 +72,7 @@ def testProjectNewRoot(nwTempProj,nwRef):
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
assert theProject.projChanged
assert theProject.saveProject()
+ assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2])
assert not theProject.projChanged
@@ -96,6 +110,8 @@ def testIndexScanThis(nwTempProj):
assert str(theBits) == "['@tag', 'this', 'and this']"
assert str(thePos) == "[0, 6, 12]"
+ assert theProject.closeProject()
+
@pytest.mark.project
def testBuildIndex(nwTempProj):
projFile = path.join(nwTempProj,"nwProject.nwx")
@@ -117,3 +133,4 @@ def testBuildIndex(nwTempProj):
assert theIndex.buildNovelList()
assert str(theIndex.novelList) == "[[1, 1, 'Novel', 'SCENE'], [3, 2, 'Chapter', 'SCENE'], [5, 3, 'Scene', 'SCENE'], [7, 4, 'Section', 'SCENE']]"
assert str(theIndex.novelOrder) == "['31489056e0916:1', '31489056e0916:3', '31489056e0916:5', '31489056e0916:7']"
+ assert theProject.closeProject()
From 144ddef579582c9fd9ab04e6420e5ec374a9448f Mon Sep 17 00:00:00 2001
From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com>
Date: Wed, 26 Feb 2020 22:18:34 +0100
Subject: [PATCH 4/4] Backup tool should not back up the lock file
---
nw/project/backup.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/nw/project/backup.py b/nw/project/backup.py
index bab5ffcc..1f2bd985 100644
--- a/nw/project/backup.py
+++ b/nw/project/backup.py
@@ -57,7 +57,9 @@ class NWBackup():
baseName = path.join(self.mainConf.backupPath, archName)
try:
+ self.theProject._clearLockFile()
make_archive(baseName, "zip", self.theProject.projPath, ".")
+ self.theProject._writeLockFile()
except Exception as e:
self.theParent.makeAlert(
["Could not write backup archive.",str(e)],