mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-18 12:04:13 +02:00
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:
@@ -1888,6 +1888,51 @@ bool QETDiagramEditor::closeProject(QETProject *project)
|
||||
return(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETDiagramEditor::askAboutLock
|
||||
Tell the user who has the project @p filepath open and let them choose.
|
||||
Read-only is the default: opening anyway can lose the other person's
|
||||
work, and a lock left by a crash on another machine cannot be told from
|
||||
a live one, which is why the age of the last sign of life is shown.
|
||||
*/
|
||||
QETDiagramEditor::LockChoice QETDiagramEditor::askAboutLock(
|
||||
const QString &filepath,
|
||||
const ProjectLock::Holder &holder)
|
||||
{
|
||||
const QString name = QFileInfo(filepath).fileName();
|
||||
QString text;
|
||||
if (holder.session == QLatin1String("?")) {
|
||||
text = tr("%1 is being opened by another session of QElectroTech.").arg(name);
|
||||
} else {
|
||||
const QString since = QLocale::system().toString(holder.since.toLocalTime(), QLocale::ShortFormat);
|
||||
text = tr("%1 is open by %2 on %3 since %4").arg(name, holder.user, holder.host, since);
|
||||
const qint64 minutes = holder.heartbeat.isValid()
|
||||
? holder.heartbeat.secsTo(QDateTime::currentDateTimeUtc()) / 60
|
||||
: -1;
|
||||
if (minutes >= 0) {
|
||||
text += minutes < 1
|
||||
? tr(" (active just now).")
|
||||
: tr(" (active %n minute(s) ago).", nullptr, static_cast<int>(minutes));
|
||||
} else {
|
||||
text += QLatin1Char('.');
|
||||
}
|
||||
}
|
||||
|
||||
QMessageBox box(QMessageBox::Warning, tr("Project in use", "message box title"), text, QMessageBox::NoButton, this);
|
||||
box.setInformativeText(tr("Open it read-only to look at it without changing it. "
|
||||
"Open it anyway only if you know that session is not "
|
||||
"using it any more: both of you could then save over each other."));
|
||||
QPushButton *read_only = box.addButton(tr("Open read-only"), QMessageBox::AcceptRole);
|
||||
QPushButton *anyway = box.addButton(tr("Open anyway"), QMessageBox::DestructiveRole);
|
||||
box.addButton(QMessageBox::Cancel);
|
||||
box.setDefaultButton(read_only);
|
||||
box.setEscapeButton(box.button(QMessageBox::Cancel));
|
||||
box.exec();
|
||||
if (box.clickedButton() == read_only) return LockChoice::ReadOnly;
|
||||
if (box.clickedButton() == anyway) return LockChoice::OpenAnyway;
|
||||
return LockChoice::Cancel;
|
||||
}
|
||||
|
||||
/**
|
||||
Ouvre un projet depuis un fichier et l'ajoute a cet editeur
|
||||
@param filepath Chemin du projet a ouvrir
|
||||
@@ -1966,6 +2011,40 @@ bool QETDiagramEditor::openAndAddProject(
|
||||
}
|
||||
}
|
||||
|
||||
//Is someone else working on this file? Taken before the project is
|
||||
//read, so a cancel costs nothing. Never blocks opening: a folder
|
||||
//the lock cannot be made in, or the setting off, opens as before.
|
||||
std::unique_ptr<ProjectLock> lock;
|
||||
bool opened_read_only_by_lock = false;
|
||||
if (ProjectLock::mayTake() && filepath_info.isWritable())
|
||||
{
|
||||
lock = std::make_unique<ProjectLock>();
|
||||
if (lock->acquire(filepath) == ProjectLock::Result::Held)
|
||||
{
|
||||
const ProjectLock::Holder holder = lock->blockingHolder();
|
||||
bool take_over = false;
|
||||
if (interactive) {
|
||||
switch (askAboutLock(filepath, holder)) {
|
||||
case LockChoice::Cancel:
|
||||
return(false);
|
||||
case LockChoice::OpenAnyway:
|
||||
take_over = true;
|
||||
break;
|
||||
case LockChoice::ReadOnly:
|
||||
break;
|
||||
}
|
||||
}
|
||||
//Not interactive (an assistant opening it): read-only, which
|
||||
//is the safe answer to a question nobody is there to ask
|
||||
if (take_over && lock->acquire(filepath, true) == ProjectLock::Result::Acquired) {
|
||||
// ours now
|
||||
} else if (!take_over) {
|
||||
lock.reset();
|
||||
opened_read_only_by_lock = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Create the project
|
||||
DialogWaiting::instance(this);
|
||||
|
||||
@@ -1998,6 +2077,17 @@ bool QETDiagramEditor::openAndAddProject(
|
||||
return(false);
|
||||
}
|
||||
|
||||
if (opened_read_only_by_lock) project->setOpenedReadOnlyByLock(true);
|
||||
if (lock && lock->isHeld()) {
|
||||
connect(lock.get(), &ProjectLock::lockTaken, this, [this, project](const ProjectLock::Holder &by) {
|
||||
statusBar()->showMessage(
|
||||
tr("%1 on %2 has taken over %3. Save your changes with Save As to keep them.")
|
||||
.arg(by.user, by.host, QFileInfo(project->filePath()).fileName()),
|
||||
30000);
|
||||
});
|
||||
project->setLock(std::move(lock));
|
||||
}
|
||||
|
||||
QETApp::projectsRecentFiles() -> fileWasOpened(filepath);
|
||||
addProject(project);
|
||||
DialogWaiting::dropInstance();
|
||||
|
||||
Reference in New Issue
Block a user