Merge pull request #179 from vkbo/lockfile

Project Lockfile
This commit is contained in:
Veronica K. Berglyd Olsen
2020-02-26 22:26:58 +01:00
committed by GitHub
7 changed files with 160 additions and 11 deletions
+5 -1
View File
@@ -20,7 +20,7 @@ from os import path, mkdir, unlink, rename
from datetime import datetime from datetime import datetime
from PyQt5.Qt import PYQT_VERSION_STR 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.constants import nwFiles, nwUnicode
from nw.common import splitVersionNumber from nw.common import splitVersionNumber
@@ -156,6 +156,10 @@ class Config:
else: else:
self.osUnknown = True self.osUnknown = True
# Other System Info
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
# Packages # Packages
self.hasEnchant = False self.hasEnchant = False
self.hasSymSpell = False self.hasSymSpell = False
+1
View File
@@ -23,6 +23,7 @@ class nwFiles():
APP_ICON = "novelWriter.svg" APP_ICON = "novelWriter.svg"
PROJ_FILE = "nwProject.nwx" PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt" PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock"
SESS_INFO = "sessionInfo.log" SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json" INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json" OPTS_FILE = "guiOptions.json"
+40 -4
View File
@@ -15,6 +15,7 @@ import time
import nw import nw
from os import path from os import path
from datetime import datetime
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence
@@ -49,9 +50,9 @@ class GuiMain(QMainWindow):
self.hasProject = False self.hasProject = False
self.isZenMode = False self.isZenMode = False
logger.info("OS: %s" % ( logger.info("OS: %s" % self.mainConf.osType)
self.mainConf.osType) logger.info("Kernel: %s" % self.mainConf.kernelVer)
) logger.info("Host: %s" % self.mainConf.hostName)
logger.info("Qt5 Version: %s (%d)" % ( logger.info("Qt5 Version: %s (%d)" % (
self.mainConf.verQtString, self.mainConf.verQtValue) self.mainConf.verQtString, self.mainConf.verQtValue)
) )
@@ -319,7 +320,42 @@ class GuiMain(QMainWindow):
# Try to open the project # Try to open the project
if not self.theProject.openProject(projFile): if not self.theProject.openProject(projFile):
return False if self.theProject.lockedBy is not None:
if self.mainConf.showGUI:
try:
lockDetails = (
"<br><br>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?<br><br>"
"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 # project is loaded
self.hasProject = True self.hasProject = True
+2
View File
@@ -57,7 +57,9 @@ class NWBackup():
baseName = path.join(self.mainConf.backupPath, archName) baseName = path.join(self.mainConf.backupPath, archName)
try: try:
self.theProject._clearLockFile()
make_archive(baseName, "zip", self.theProject.projPath, ".") make_archive(baseName, "zip", self.theProject.projPath, ".")
self.theProject._writeLockFile()
except Exception as e: except Exception as e:
self.theParent.makeAlert( self.theParent.makeAlert(
["Could not write backup archive.",str(e)], ["Could not write backup archive.",str(e)],
+90 -1
View File
@@ -40,6 +40,7 @@ class NWProject():
self.projOpened = None # The time stamp of when the project file was opened self.projOpened = None # The time stamp of when the project file was opened
self.projChanged = None # The project has unsaved changes self.projChanged = None # The project has unsaved changes
self.projAltered = None # The project has been altered this session self.projAltered = None # The project has been altered this session
self.lockedBy = None # Data on which computer has the project open
# Debug # Debug
self.handleSeed = None self.handleSeed = None
@@ -178,7 +179,7 @@ class NWProject():
return return
def openProject(self, fileName): def openProject(self, fileName, overrideLock=False):
"""Open the project file provided, or if doesn't exist, assume """Open the project file provided, or if doesn't exist, assume
it is a folder, and look for the file within it. If successful, 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 parse the XML of the file and populate the project variables and
@@ -201,6 +202,21 @@ class NWProject():
if not self._checkFolder(self.projMeta): if not self._checkFolder(self.projMeta):
return 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: try:
projectMaintenance(self) projectMaintenance(self)
except Exception as E: except Exception as E:
@@ -308,6 +324,7 @@ class NWProject():
self.setProjectChanged(False) self.setProjectChanged(False)
self.projOpened = time() self.projOpened = time()
self.projAltered = False self.projAltered = False
self._writeLockFile()
return True return True
@@ -398,14 +415,19 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime) self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
self.mainConf.saveRecentCache() self.mainConf.saveRecentCache()
self._writeLockFile()
self.theParent.setStatus("Saved Project: %s" % self.projName) self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False) self.setProjectChanged(False)
return True return True
def closeProject(self): def closeProject(self):
"""Close the current project and clear all meta data.
"""
self._appendSessionStats() self._appendSessionStats()
self._clearLockFile()
self.clearProject() self.clearProject()
self.lockedBy = None
return True return True
## ##
@@ -645,6 +667,73 @@ class NWProject():
# Internal Functions # 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): def _checkFolder(self, thePath):
if not path.isdir(thePath): if not path.isdir(thePath):
try: try:
+5 -5
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-08 20:53:03"> <novelWriterXML appVersion="0.4.5" fileVersion="1.0" timeStamp="2020-02-26 22:05:41">
<project> <project>
<name></name> <name></name>
<title></title> <title></title>
@@ -72,28 +72,28 @@
<paraCount>0</paraCount> <paraCount>0</paraCount>
<cursorPos>0</cursorPos> <cursorPos>0</cursorPos>
</item> </item>
<item handle="39fa9ec190eee" order="None" parent="None"> <item handle="8722616204217" order="None" parent="None">
<name>Timeline</name> <name>Timeline</name>
<type>ROOT</type> <type>ROOT</type>
<class>TIMELINE</class> <class>TIMELINE</class>
<status>New</status> <status>New</status>
<expanded>False</expanded> <expanded>False</expanded>
</item> </item>
<item handle="d029fa3a95e17" order="None" parent="None"> <item handle="96061e92f58e4" order="None" parent="None">
<name>Object</name> <name>Object</name>
<type>ROOT</type> <type>ROOT</type>
<class>OBJECT</class> <class>OBJECT</class>
<status>New</status> <status>New</status>
<expanded>False</expanded> <expanded>False</expanded>
</item> </item>
<item handle="81b8a03f97e87" order="None" parent="None"> <item handle="eb624dbe56eb6" order="None" parent="None">
<name>Custom1</name> <name>Custom1</name>
<type>ROOT</type> <type>ROOT</type>
<class>CUSTOM</class> <class>CUSTOM</class>
<status>New</status> <status>New</status>
<expanded>False</expanded> <expanded>False</expanded>
</item> </item>
<item handle="da4ea2a5506f2" order="None" parent="None"> <item handle="f369cb89fc627" order="None" parent="None">
<name>Custom2</name> <name>Custom2</name>
<type>ROOT</type> <type>ROOT</type>
<class>CUSTOM</class> <class>CUSTOM</class>
+17
View File
@@ -29,6 +29,7 @@ def testProjectNew(nwTempProj,nwRef,nwTemp):
assert theProject.newProject() assert theProject.newProject()
assert theProject.setProjectPath(nwTempProj) assert theProject.setProjectPath(nwTempProj)
assert theProject.saveProject() assert theProject.saveProject()
assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2]) assert cmpFiles(projFile, refFile, [2])
@pytest.mark.project @pytest.mark.project
@@ -41,9 +42,21 @@ def testProjectSave(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx") projFile = path.join(nwTempProj,"nwProject.nwx")
refFile = path.join(nwRef,"proj","1_nwProject.nwx") refFile = path.join(nwRef,"proj","1_nwProject.nwx")
assert theProject.saveProject() assert theProject.saveProject()
assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2]) assert cmpFiles(projFile, refFile, [2])
assert not theProject.projChanged 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 @pytest.mark.project
def testProjectNewRoot(nwTempProj,nwRef): def testProjectNewRoot(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx") projFile = path.join(nwTempProj,"nwProject.nwx")
@@ -59,6 +72,7 @@ def testProjectNewRoot(nwTempProj,nwRef):
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str) assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
assert theProject.projChanged assert theProject.projChanged
assert theProject.saveProject() assert theProject.saveProject()
assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2]) assert cmpFiles(projFile, refFile, [2])
assert not theProject.projChanged assert not theProject.projChanged
@@ -96,6 +110,8 @@ def testIndexScanThis(nwTempProj):
assert str(theBits) == "['@tag', 'this', 'and this']" assert str(theBits) == "['@tag', 'this', 'and this']"
assert str(thePos) == "[0, 6, 12]" assert str(thePos) == "[0, 6, 12]"
assert theProject.closeProject()
@pytest.mark.project @pytest.mark.project
def testBuildIndex(nwTempProj): def testBuildIndex(nwTempProj):
projFile = path.join(nwTempProj,"nwProject.nwx") projFile = path.join(nwTempProj,"nwProject.nwx")
@@ -117,3 +133,4 @@ def testBuildIndex(nwTempProj):
assert theIndex.buildNovelList() 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.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 str(theIndex.novelOrder) == "['31489056e0916:1', '31489056e0916:3', '31489056e0916:5', '31489056e0916:7']"
assert theProject.closeProject()