Files
qelectrotech-source-mirror/sources/projectlock.h
T
ispyisail 3521c64ba3 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>
2026-10-16 13:01:54 +13:00

117 lines
3.7 KiB
C++

/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PROJECTLOCK_H
#define PROJECTLOCK_H
#include <QDateTime>
#include <QObject>
#include <QString>
#include <QTimer>
/**
@brief The ProjectLock class
A sidecar file beside a project that says who has it open, so that a second
person (or a second QElectroTech) opening the same file on a shared drive
is told, instead of both editing and the last save wiping the other's work.
The file is ".~<filename>.lock" in the project's folder, a small versioned
JSON object. It is not the project file itself: a project is saved by
writing a temporary file and renaming it over the old one, which would
replace a lock kept in the project at every save.
Taking the lock is an exclusive create (QFile NewOnly: O_EXCL, or
CREATE_NEW on Windows), the one operation shared drives do reliably. This is
not a QLockFile: its stale time defaults to 30 s, and for a lock made on
another machine it decides by age alone, so another PC would take a lock
held for over 30 s. Here a lock is stale only when it was made on this
machine by a process that is gone; any other lock is shown to the user, with
the age of its heartbeat, to judge.
*/
class ProjectLock : public QObject
{
Q_OBJECT
public:
/// What a lock file says
struct Holder
{
bool valid = false; ///< false: the file could not be read as a lock
QString session;
QString user;
QString host;
qint64 pid = 0;
QString app;
QDateTime since;
QDateTime heartbeat;
};
enum class Result {
Acquired, ///< the lock is ours
Held, ///< a live lock of another session is there
Unavailable ///< the lock file cannot be made (read-only folder, ...)
};
explicit ProjectLock(QObject *parent = nullptr);
~ProjectLock() override;
Result acquire(const QString &project_path, bool take_over = false);
bool moveTo(const QString &project_path);
void release();
bool refresh();
bool isHeld() const {return m_held;}
QString projectPath() const {return m_project_path;}
QString session() const {return m_session;}
Holder blockingHolder() const {return m_blocking;}
static QString lockPathFor(const QString &project_path);
static Holder readHolder(const QString &project_path);
static bool isStale(const Holder &holder);
static Holder liveHolder(const QString &project_path, const QString &own_session = QString());
static bool enabled();
static void setEnabled(bool);
static bool mayTake();
static void setHeadless(bool);
static void setHeartbeatInterval(int msec);
static QString thisHost();
static QString thisUser();
signals:
/// Another session took the lock over (its "Open anyway")
void lockTaken(const ProjectLock::Holder &by);
private:
QByteArray content(const QDateTime &since, const QDateTime &heartbeat) const;
bool createExclusive(const QString &lock_path);
static void hide(const QString &lock_path);
QString m_project_path;
QString m_session;
QDateTime m_since;
bool m_held = false;
Holder m_blocking;
QTimer m_timer;
};
Q_DECLARE_METATYPE(ProjectLock::Holder)
#endif