Tell a second person who has a project open, and open it read-only

Two people opening the same project on a shared drive could both edit it,
and the last save silently wiped the other's work. A project now has a
hidden lock file beside it (.~plant.qetz.lock, small JSON: user, host,
process, version, since, heartbeat). Opening a project another live session
holds shows who has it, since when and how long ago they were last active,
and offers Open read-only (default), Open anyway, or Cancel.

- The lock is made with an exclusive create, refreshed every minute and
  deleted when the project closes. It is not a QLockFile: that treats any
  lock from another machine as stale after 30 s. Here only a lock made on
  this machine by a process that no longer exists is taken silently.
- A folder the lock cannot be made in, or the setting turned off
  (Settings > General > Projects), opens as before.
- Save As and .qet saved as .qetz move the lock to the new file, and refuse
  a file someone else has open. A project opened read-only for another's
  lock does not save over that file.
- --resave and --set-titleblock refuse a file someone has open unless given
  --ignore-lock; the read-only verbs never look at locks and headless runs
  never take one. A script's qet.save() refuses the same way.

Checked: 17 new unit tests (5 go red when stale detection and the exclusive
create are broken), full ctest 107/107, the dialog and both choices in the
GUI under Xvfb, and --resave refused/allowed from the command line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-16 13:01:54 +13:00
parent c4b5cf2f11
commit 3521c64ba3
17 changed files with 1012 additions and 0 deletions
+24
View File
@@ -383,6 +383,9 @@ QString ProjectView::askUserForFilePath(bool assign) {
}
filepath += zipped ? ".qetz" : ".qet";
//Not onto a file someone else has open
if (!refuseIfLockedByOther(filepath)) return(QString());
if (assign) {
// assign the provided filepath to the currently edited project
m_project -> setFilePath(filepath);
@@ -391,6 +394,26 @@ QString ProjectView::askUserForFilePath(bool assign) {
return(filepath);
}
/**
@brief ProjectView::refuseIfLockedByOther
@return false, after telling the user, when another session holds the
lock of the file @p filepath this project is about to be saved to
*/
bool ProjectView::refuseIfLockedByOther(const QString &filepath)
{
if (!ProjectLock::mayTake()) return(true);
const ProjectLock::Holder holder = ProjectLock::liveHolder(
filepath, m_project && m_project->lock() ? m_project->lock()->session() : QString());
if (!holder.valid) return(true);
QMessageBox::warning(
this,
tr("Project in use", "message box title"),
tr("%1 is open by %2 on %3. Choose another file name, or save again "
"once it has been closed there.")
.arg(QFileInfo(filepath).fileName(), holder.user, holder.host));
return(false);
}
/**
@return the QETResult object to be returned when it appears this project
view is not associated to any project.
@@ -773,6 +796,7 @@ QETResult ProjectView::doSave()
//and ask before replacing a .qetz already there
if (m_project->savesBesideOpenedFile()) {
const QString beside = m_project->besideContainerPath();
if (!refuseIfLockedByOther(beside)) return(QETResult());
const QString message = tr("This project was opened from %1. It is saved as %2, "
"beside it, in the zipped project format; %1 stays as "
"it is.\n\nTo write a file for a version of "