[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-games-devel
Subject:    [Kde-games-devel] KHighscore on multiple user systems
From:       Nicolas Hadacek <hadacek () kde ! org>
Date:       2003-05-11 15:50:14
[Download RAW message or body]

Hi,

[I am cross-posting kde-core since there are some issues about "setgid" 
programs and I think more knowlegeable people are on this list.]

There was from long time ago some support for using a global highscore file 
for every user on a system but there were still two problems to solve. The 
provided patch resolves both but I'd like to have some comments:

1. the global highscore file ("mygame.scores" usually in /var/games) should 
allow write access to the "games" group (a user must not be able to modify 
it...). To modify it, a game program should be "setgid games". My solution is 
to drop this effective group id as soon as possible (before creating 
KApplication) and then to renable it shortly when needed.

2. there should be a locking mechanism for modifying the file so that 
different users playing at the same time do not mangle everything...
I am implementing that with "flock()".
The complete procedure is:

set "games" group id
open global file read only
flock it (if possible)
drop "games" group id
create a temporary file (in /tmp) and copy the global file in it
create a KSimpleConfig on the temp file
...
write new highscores data
...
commit changes to the temporary file
set "games" gid
truncate global file
copy temporary file to global file
drop "games" gid
remove temporary file
unlock global file


I have done some tests which are quite succesful...
What still need to be addressed is having the program executable being "setgig 
games" at installation and creating the global highscore file with the 
correct permissions if it does not exist.

Any comments ?

see you,
Nicolas

["khighscore.diff" (text/x-diff)]

Index: khighscore.h
===================================================================
RCS file: /home/kde/kdegames/libkdegames/highscore/khighscore.h,v
retrieving revision 1.7
diff -u -2 -d -p -b -r1.7 khighscore.h
--- khighscore.h	6 Jan 2002 18:05:53 -0000	1.7
+++ khighscore.h	12 May 2003 03:29:16 -0000
@@ -23,8 +23,11 @@
 #define __KHIGHSCORE_H__
 
+#include <sys/types.h>
+
 #include <qstring.h>
 #include <qobject.h>
 
 class KConfig;
+class KTempFile;
 
 class KHighscorePrivate;
@@ -32,5 +35,5 @@ class KHighscorePrivate;
  * This is the KDE class for saving and reading highscore tables. It offers the
  * possibility for system-wide highscore tables (configure with e.g.
- * --enable-highscore=/var/games) and a theoretically unlimited number of
+ * --enable-highscore-dir=/var/games) and a theoretically unlimited number of
  * entries.
  *
@@ -68,5 +71,5 @@ class KHighscorePrivate;
  * </pre>
  *
- * Also note that yout MUST NOT mark the key or the group for translation! I.e. \
don't use + * Also note that you MUST NOT mark the key or the group for translation! \
                I.e. don't use
  * i18n() for the keys or groups! Here is the code to read the above written entry:
  * <pre>
@@ -81,5 +84,77 @@ class KHighscore : public QObject
 	Q_OBJECT
 public:
+        /**
+         * Constructor. The highscore file is either local or global depending
+         * on the configuration.
+         */
 	KHighscore(QObject* parent = 0);
+
+        /**
+         * Constructor.
+         * @param forceLocal if true, the local highscore file is used even
+         * when the configuration has been set to use a global file. This is
+         * convenient for converting highscores from legacy applications.
+         * @since 3.2
+         */
+        KHighscore(bool forceLocal, QObject *parent);
+
+        /**
+         * Read the current state of the highscore file. Remember that when
+         * it's not locked for writing, this file can change at any time.
+         * (This method is only useful for a global highscore file).
+         # @since 3.2
+         */
+        void readCurrentConfig();
+
+        /**
+         * This method is used to store the effective group id of the
+         * game (which should be "games" to access the global highscore file)
+         * and then to drop such special privilege.
+         * This effective group id is shortly used to lock the global highscore
+         * file, and for writing and unlocking it.
+         *
+         * Note: this method should be called in main() before creating a
+         * KApplication (KApplication checks that the program is not suid/sgid
+         * and will exit the program if it is the case).
+         * @since 3.2
+         */
+        static void init();
+
+        /**
+         * Lock the global highscore file for writing (does nothing and
+         * return true if the local file file is used).
+         * You should perform writing without GUI interaction to avoid
+         * blocking and don't forget to unlock the file as soon as possible
+         * with @ref writeAndUnlock().
+         *
+         * If the config file cannot be locked,
+         * the method waits for 1 second and, if it failed again, displays
+         * a message box asking for retry or cancel.
+         * @param widget used as the parent of the message box.
+         *
+         * @return false on error or if the config file is locked by another
+         * process. In such case, the config file stays read-only.
+         * @since 3.2
+         */
+        bool lockForWriting(QWidget *widget = 0);
+
+        /**
+         * Effectively write and unlock the global highscore file.
+         * @see lockForWriting.
+         * If using a local highscore file, it will sync the config.
+         * @since 3.2
+         */
+        void writeAndUnlock();
+
+        /** @return if the highscore file is locked (return true if a local
+         * file is used).
+         * @since 3.2
+         */
+        bool isLocked() const;
+
+        /**
+         * Destructor.
+         * If necessary, unlock on the highscore file.
+         */
 	~KHighscore();
 
@@ -188,4 +263,7 @@ public:
 	bool hasTable() const;
 
+        /** This does the same as writeAndUnlock().
+         * @obsolete
+         */
 	void sync();
 
@@ -215,11 +293,24 @@ protected:
 	/**
 	 * @return A pointer to the @ref KConfig object to be used. This is
-	 * either kapp->config() (default) or a @ref KSimpleConfig object (if
-	 * you configured with --enable-highscore)
+	 * either kapp->config() (default) or a @ref KSimpleConfig object for
+         * a global highscore file.
 	 **/
 	KConfig* config() const;
 
+        /**
+         * @return the global highscore filename.
+         * @since 3.2
+         */
+        QString globalConfigFilename() const;
+
+        static void unlockInternal();
+        static bool file_copy(int src, int dest);
+
 private:
 	KHighscorePrivate* d;
+
+        static gid_t _egid; // the effective group id
+        static int _fd;     // global highscore handle when locked
+        static KTempFile *_tmpFile; // temporary file used for writing
 };
 
Index: khighscore.cpp
===================================================================
RCS file: /home/kde/kdegames/libkdegames/highscore/khighscore.cpp,v
retrieving revision 1.15
diff -u -2 -d -p -b -r1.15 khighscore.cpp
--- khighscore.cpp	25 Feb 2003 01:03:18 -0000	1.15
+++ khighscore.cpp	12 May 2003 03:29:17 -0000
@@ -21,7 +21,15 @@
 */
 
+#include <unistd.h>
+#include <sys/file.h>
+
 #include <kapplication.h>
 #include <ksimpleconfig.h>
 #include <kglobal.h>
+#include <kstdguiitem.h>
+#include <klocale.h>
+#include <kmessagebox.h>
+#include <kdebug.h>
+#include <ktempfile.h>
 
 #include "khighscore.h"
@@ -33,26 +41,188 @@ class KHighscorePrivate
 {
 public:
-	KHighscorePrivate()
-	{
-		mConfig = 0;
-	}
+    KHighscorePrivate() : mConfig(0) {}
 
 	KConfig* mConfig;
 	QString group;
+    bool    global;
 };
 
-KHighscore::KHighscore(QObject* parent) : QObject(parent)
+int KHighscore::_fd = -1;
+gid_t KHighscore::_egid = 0;
+KTempFile *KHighscore::_tmpFile = 0;
+
+KHighscore::KHighscore(QObject* parent)
+    : QObject(parent)
 {
  d = new KHighscorePrivate;
+#ifdef HIGHSCORE_DIRECTORY
+    d->global = true;
+#else
+    d->global = false;
+#endif
+    readCurrentConfig();
+}
 
+KHighscore::KHighscore(bool forceLocal, QObject* parent)
+    : QObject(parent)
+{
+    d = new KHighscorePrivate;
+#ifdef HIGHSCORE_DIRECTORY
+    d->global = !forceLocal;
+#else
+    d->global = false;
+#endif
+    readCurrentConfig();
 }
 
-KHighscore::~KHighscore()
+
+void KHighscore::readCurrentConfig()
 {
-// not necessary, as KConfig destructor should handle this
- sync();
- if (d->mConfig) {
+    if (d->global) {
+        delete d->mConfig;
+        // read only KSimpleConfig
+        d->mConfig = new KSimpleConfig(globalConfigFilename(), true);
+    }
+}
+
+void KHighscore::init()
+{
+    _egid = getegid(); // store effective group id
+    setgid(getgid()); // drop effective group id
+}
+
+bool KHighscore::file_copy(int src, int dest)
+{
+    enum { BUFFER_LEN = 1024 };
+    static char BUFFER[BUFFER_LEN];
+    for (;;) {
+        int n = ::read(src, BUFFER, BUFFER_LEN);
+        if ( n==-1 ) {
+            if ( errno==EINTR ) continue;
+            kdWarning(11002) << "cannot read file" << endl;
+            return false;
+        }
+        if ( n==0 ) break; // done
+        char *ptr = BUFFER;
+        while ( n>0 ) {
+            ssize_t written = ::write(dest, ptr, n);
+            if ( written<0 ) {
+                if ( errno==EINTR ) continue;
+                kdWarning(11002) << "cannot write file" << endl;
+                return false;
+            }
+            ptr += written;
+            n -= written;
+        }
+    }
+    return true;
+}
+
+bool KHighscore::lockForWriting(QWidget *widget)
+{
+    if ( !d->global || isLocked() ) return true;
+
+    bool first = true;
+    QString filename = globalConfigFilename();
+    for (;;) {
+
+        //----------------------------------------
+        setegid(_egid); // use effective group id
+        // lock the highscore file (it should exist)
+        _fd = open(filename.local8Bit(), O_RDWR);
+        bool ok = ( _fd>=0 && flock(_fd, LOCK_EX | LOCK_NB)>=0 );
+        setgid(getgid()); // drop effective group id
+        //----------------------------------------
+
+        kdDebug(11002) << "locking global highscore file ok=" << ok << endl;
+        if (!ok) unlockInternal();
+        else {
+            // copy global highscore file to a temp file
+            _tmpFile = new KTempFile;
+            if ( !file_copy(_fd, _tmpFile->handle()) ) {
+                kdWarning(11002) << "cannot copy the global highscore file to "
+                                 << "a temporary file" << endl;
+                unlockInternal();
+                return false;
+            }
+
+            // create read-write KSimpleConfig
 	delete d->mConfig;
+            d->mConfig = new KSimpleConfig(_tmpFile->name());
+            return true;
+        }
+
+        if ( !first ) {
+            KGuiItem item = KStdGuiItem::cont();
+            item.setText(i18n("Retry"));
+            int res = KMessageBox::warningContinueCancel(widget, i18n("Cannot access \
the highscore file. Another user is probably currently writing to it."), \
QString::null, item, "ask_lock_global_highscore_file"); +            if ( \
res==KMessageBox::Cancel ) break; +        } else sleep(1);
+        first = false;
+    }
+    return false;
+}
+
+void KHighscore::unlockInternal()
+{
+    // remove tmp file
+    if (_tmpFile) {
+        _tmpFile->unlink();
+        delete _tmpFile;
+        _tmpFile = 0;
  }
+
+    // unlock highscore file
+    if ( _fd!=-1 ) {
+        close(_fd);
+        _fd = -1;
+    }
+}
+
+void KHighscore::writeAndUnlock()
+{
+    if ( !d->global ) {
+        kapp->config()->sync();
+        return;
+    }
+
+    if ( !isLocked () ) return;
+
+    delete d->mConfig; // write config to temporary file
+    _tmpFile->close();
+    int src = open(_tmpFile->name().local8Bit(), O_RDONLY);
+
+    //---------------------------------------
+    setegid(_egid); // use effective group id
+    lseek(_fd, 0, SEEK_SET); // set offset to begin of file
+    ftruncate(_fd, 0); // erase old content
+    if ( !file_copy(src, _fd) ) // copy tmp file to highscore file
+        kdWarning(11002) << "cannot copy the temporary file"
+                         << " to the global highscore file" << endl;
+    setgid(getgid()); // drop effective group id
+    //---------------------------------------
+
+    close(src);
+    unlockInternal(); // clean an unlock
+
+    // read-only KSimpleConfig
+    d->mConfig = new KSimpleConfig(globalConfigFilename(), true);
+}
+
+bool KHighscore::isLocked() const
+{
+    return (d->global ? _fd!=-1 : true);
+}
+
+QString KHighscore::globalConfigFilename() const
+{
+    return QString::fromLocal8Bit("%1/%2.scores").arg(HIGHSCORE_DIRECTORY)
+        .arg(KGlobal::instance()->instanceName());
+}
+
+KHighscore::~KHighscore()
+{
+    writeAndUnlock();
+    delete d->mConfig;
  delete d;
 }
@@ -60,20 +230,10 @@ KHighscore::~KHighscore()
 KConfig* KHighscore::config() const
 {
- #ifdef HIGHSCORE_DIRECTORY
-	if (!d->mConfig) {
-		//AB: is instanceName() correct? MUST be the same for all
-		//processes of the game!
-		QString file=QString::fromLocal8Bit("%1/%2").arg(HIGHSCORE_DIRECTORY).arg(KGlobal::instance()->instanceName());
                
-		d->mConfig = new KSimpleConfig(file);
-	}
-	return d->mConfig;
- #else
-	return kapp->config();
- #endif
+    return (d->global ? d->mConfig : kapp->config());
 }
 
 void KHighscore::writeEntry(int entry, const QString& key, const QVariant& value)
 {
- // save the group in case that we are on kapp->config()
+ Q_ASSERT( isLocked() );
  KConfigGroupSaver cg(config(), group());
  QString confKey = QString("%1_%2").arg(entry).arg(key);
@@ -83,5 +243,5 @@ void KHighscore::writeEntry(int entry, c
 void KHighscore::writeEntry(int entry, const QString& key, int value)
 {
- // save the group in case that we are on kapp->config()
+ Q_ASSERT( isLocked() );
  KConfigGroupSaver cg(config(), group());
  QString confKey = QString("%1_%2").arg(entry).arg(key);
@@ -91,4 +251,5 @@ void KHighscore::writeEntry(int entry, c
 void KHighscore::writeEntry(int entry, const QString& key, const QString &value)
 {
+ Q_ASSERT (isLocked() );
  KConfigGroupSaver cg(config(), group());
  QString confKey = QString("%1_%2").arg(entry).arg(key);
@@ -152,9 +313,8 @@ const QString& KHighscore::highscoreGrou
 QString KHighscore::group() const
 {
- if (highscoreGroup().isNull()) {
-	return GROUP;
- } else {
-	return QString("%1_%2").arg(GROUP).arg(highscoreGroup());
- }
+    if ( highscoreGroup().isNull() )
+        return (d->global ? QString::null : GROUP);
+    return (d->global ? highscoreGroup()
+            : QString("%1_%2").arg(GROUP).arg(highscoreGroup()));
 }
 
@@ -163,5 +323,7 @@ bool KHighscore::hasTable() const
 
 void KHighscore::sync()
-{ config()->sync(); }
+{
+    writeAndUnlock();
+}
 
 #include "khighscore.moc"



_______________________________________________
kde-games-devel mailing list
kde-games-devel@mail.kde.org
http://mail.kde.org/mailman/listinfo/kde-games-devel


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic