kio Library API Documentation

kdirwatch.cpp

00001 // -*- c-basic-offset: 2 -*-
00002 /* This file is part of the KDE libraries
00003    Copyright (C) 1998 Sven Radej <sven@lisa.exp.univie.ac.at>
00004 
00005    This library is free software; you can redistribute it and/or
00006    modify it under the terms of the GNU Library General Public
00007    License version 2 as published by the Free Software Foundation.
00008 
00009    This library is distributed in the hope that it will be useful,
00010    but WITHOUT ANY WARRANTY; without even the implied warranty of
00011    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
00012    Library General Public License for more details.
00013 
00014    You should have received a copy of the GNU Library General Public License
00015    along with this library; see the file COPYING.LIB.  If not, write to
00016    the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
00017    Boston, MA 02111-1307, USA.
00018 */
00019 
00020 
00021 // CHANGES:
00022 // Februar 2002 - Add file watching and remote mount check for STAT
00023 // Mar 30, 2001 - Native support for Linux dir change notification.
00024 // Jan 28, 2000 - Usage of FAM service on IRIX (Josef.Weidendorfer@in.tum.de)
00025 // May 24. 1998 - List of times introduced, and some bugs are fixed. (sven)
00026 // May 23. 1998 - Removed static pointer - you can have more instances.
00027 // It was Needed for KRegistry. KDirWatch now emits signals and doesn't
00028 // call (or need) KFM. No more URL's - just plain paths. (sven)
00029 // Mar 29. 1998 - added docs, stop/restart for particular Dirs and
00030 // deep copies for list of dirs. (sven)
00031 // Mar 28. 1998 - Created.  (sven)
00032 
00033 
00034 #include <config.h>
00035 
00036 #ifdef HAVE_DNOTIFY
00037 //#include <sys/stat.h>
00038 #include <unistd.h>
00039 //#include <time.h>
00040 #include <fcntl.h>
00041 #include <signal.h>
00042 #include <errno.h>
00043 #endif
00044 
00045 #include <assert.h>
00046 #include <qdir.h>
00047 #include <qfile.h>
00048 #include <qintdict.h>
00049 #include <qptrlist.h>
00050 #include <qsocketnotifier.h>
00051 #include <qstringlist.h>
00052 #include <qtimer.h>
00053 
00054 #include <kapplication.h>
00055 #include <kdebug.h>
00056 #include <kconfig.h>
00057 #include <kglobal.h>
00058 #include <kstaticdeleter.h>
00059 
00060 #include "kdirwatch.h"
00061 #include "kdirwatch_p.h"
00062 #include "global.h" //  KIO::probably_slow_mounted
00063 
00064 #define NO_NOTIFY (time_t) 0
00065 
00066 static KDirWatchPrivate* dwp_self = 0;
00067 
00068 #ifdef HAVE_DNOTIFY
00069 
00070 #include <sys/utsname.h>
00071 
00072 static int dnotify_signal = 0;
00073 
00074 /* DNOTIFY signal handler
00075  *
00076  * As this is called asynchronously, only a flag is set and
00077  * a rescan is requested.
00078  * This is done by writing into a pipe to trigger a QSocketNotifier
00079  * watching on this pipe: a timer is started and after a timeout,
00080  * the rescan is done.
00081  */
00082 void KDirWatchPrivate::dnotify_handler(int, siginfo_t *si, void *)
00083 {
00084   // write might change errno, we have to save it and restore it
00085   // (Richard Stevens, Advanced programming in the Unix Environment)
00086   int saved_errno = errno;
00087 
00088   Entry* e = (dwp_self) ? dwp_self->fd_Entry.find(si->si_fd) :0;
00089 
00090 //  kdDebug(7001) << "DNOTIFY Handler: fd " << si->si_fd << " path "
00091 //      << QString(e ? e->path:"unknown") << endl;
00092 
00093   if(!e || e->dn_fd != si->si_fd) {
00094     qDebug("fatal error in KDirWatch");
00095   } else
00096     e->dn_dirty = true;
00097 
00098   char c = 0;
00099   write(dwp_self->mPipe[1], &c, 1);
00100   errno = saved_errno;
00101 }
00102 
00103 static struct sigaction old_sigio_act;
00104 /* DNOTIFY SIGIO signal handler
00105  *
00106  * When the kernel queue for the dnotify_signal overflows, a SIGIO is send.
00107  */
00108 void KDirWatchPrivate::dnotify_sigio_handler(int sig, siginfo_t *si, void *p)
00109 {
00110   // write might change errno, we have to save it and restore it
00111   // (Richard Stevens, Advanced programming in the Unix Environment)
00112   int saved_errno = errno;
00113 
00114   if (dwp_self) 
00115     dwp_self->rescan_all = true;
00116 
00117   char c = 0;
00118   write(dwp_self->mPipe[1], &c, 1);
00119 
00120   errno = saved_errno;
00121   
00122   // Call previous signal handler
00123   if (old_sigio_act.sa_flags & SA_SIGINFO)
00124   {
00125     if (old_sigio_act.sa_sigaction)
00126       (*old_sigio_act.sa_sigaction)(sig, si, p);
00127   }
00128   else
00129   {
00130     if ((old_sigio_act.sa_handler != SIG_DFL) &&
00131         (old_sigio_act.sa_handler != SIG_IGN))
00132       (*old_sigio_act.sa_handler)(sig);
00133   }
00134 }
00135 #endif
00136 
00137 
00138 //
00139 // Class KDirWatchPrivate (singleton)
00140 //
00141 
00142 /* All entries (files/directories) to be watched in the
00143  * application (coming from multiple KDirWatch instances)
00144  * are registered in a single KDirWatchPrivate instance.
00145  *
00146  * At the moment, the following methods for file watching
00147  * are supported:
00148  * - Polling: All files to be watched are polled regularly
00149  *   using stat (more precise: QFileInfo.lastModified()).
00150  *   The polling frequency is determined from global kconfig
00151  *   settings, defaulting to 500 ms for local directories
00152  *   and 5000 ms for remote mounts
00153  * - FAM (File Alternation Monitor): first used on IRIX, SGI
00154  *   has ported this method to LINUX. It uses a kernel part
00155  *   (IMON, sending change events to /dev/imon) and a user
00156  *   level damon (fam), to which applications connect for
00157  *   notification of file changes. For NFS, the fam damon
00158  *   on the NFS server machine is used; if IMON is not built
00159  *   into the kernel, fam uses polling for local files.
00160  * - DNOTIFY: In late LINUX 2.3.x, directory notification was
00161  *   introduced. By opening a directory, you can request for
00162  *   UNIX signals to be sent to the process when a directory
00163  *   is changed.
00164  */
00165 
00166 KDirWatchPrivate::KDirWatchPrivate()
00167 {
00168   timer = new QTimer(this);
00169   connect (timer, SIGNAL(timeout()), this, SLOT(slotRescan()));
00170   freq = 3600000; // 1 hour as upper bound
00171   statEntries = 0;
00172   delayRemove = false;
00173   m_ref = 0;
00174 
00175   KConfigGroup config(KGlobal::config(), QCString("DirWatch"));
00176   m_nfsPollInterval = config.readNumEntry("NFSPollInterval", 5000);
00177   m_PollInterval = config.readNumEntry("PollInterval", 500);
00178 
00179   QString available("Stat");
00180 
00181 #ifdef HAVE_FAM
00182   // It's possible that FAM server can't be started
00183   if (FAMOpen(&fc) ==0) {
00184     available += ", FAM";
00185     use_fam=true;
00186     sn = new QSocketNotifier( FAMCONNECTION_GETFD(&fc),
00187                   QSocketNotifier::Read, this);
00188     connect( sn, SIGNAL(activated(int)),
00189          this, SLOT(famEventReceived()) );
00190   }
00191   else {
00192     kdDebug(7001) << "Can't use FAM (fam daemon not running?)" << endl;
00193     use_fam=false;
00194   }
00195 #endif
00196 
00197 #ifdef HAVE_DNOTIFY
00198   supports_dnotify = true; // not guilty until proven guilty
00199   rescan_all = false;
00200   struct utsname uts;
00201   int major, minor, patch;
00202   if (uname(&uts) < 0)
00203     supports_dnotify = false; // *shrug*
00204   else if (sscanf(uts.release, "%d.%d.%d", &major, &minor, &patch) != 3)
00205     supports_dnotify = false; // *shrug*
00206   else if( major * 1000000 + minor * 1000 + patch < 2004019 ) { // <2.4.19
00207     kdDebug(7001) << "Can't use DNotify, Linux kernel too old" << endl;
00208     supports_dnotify = false; 
00209   }
00210 
00211   if( supports_dnotify ) {
00212     available += ", DNotify";
00213 
00214     pipe(mPipe);
00215     fcntl(mPipe[0], F_SETFD, FD_CLOEXEC);
00216     fcntl(mPipe[1], F_SETFD, FD_CLOEXEC);
00217     mSn = new QSocketNotifier( mPipe[0], QSocketNotifier::Read, this);
00218     connect(mSn, SIGNAL(activated(int)), this, SLOT(slotActivated()));
00219     connect(&mTimer, SIGNAL(timeout()), this, SLOT(slotRescan()));
00220     struct sigaction act;
00221     act.sa_sigaction = KDirWatchPrivate::dnotify_handler;
00222     sigemptyset(&act.sa_mask);
00223     act.sa_flags = SA_SIGINFO;
00224 #ifdef SA_RESTART
00225     act.sa_flags |= SA_RESTART;
00226 #endif
00227     if( dnotify_signal == 0 )
00228         dnotify_signal = SIGRTMIN + 8;
00229     sigaction(dnotify_signal, &act, NULL);
00230     
00231     act.sa_sigaction = KDirWatchPrivate::dnotify_sigio_handler;
00232     sigaction(SIGIO, &act, &old_sigio_act);
00233   }
00234 #endif
00235 
00236   kdDebug(7001) << "Available methods: " << available << endl;
00237 }
00238 
00239 /* This should never be called, but doesn't harm */
00240 KDirWatchPrivate::~KDirWatchPrivate()
00241 {
00242   timer->stop();
00243 
00244   /* remove all entries being watched */
00245   removeEntries(0);
00246 
00247 #ifdef HAVE_FAM
00248   if (use_fam) {
00249     FAMClose(&fc);
00250     kdDebug(7001) << "KDirWatch deleted (FAM closed)" << endl;
00251   }
00252 #endif
00253 
00254 }
00255 
00256 #ifdef HAVE_DNOTIFY
00257 void KDirWatchPrivate::slotActivated()
00258 {
00259    char dummy_buf[100];
00260    read(mPipe[0], &dummy_buf, 100);
00261 
00262    if (!mTimer.isActive())
00263       mTimer.start(200, true);
00264 }
00265 
00266 /* In DNOTIFY mode, only entries which are marked dirty are scanned.
00267  * We first need to mark all yet nonexistant, but possible created
00268  * entries as dirty...
00269  */
00270 void KDirWatchPrivate::Entry::propagate_dirty()
00271 {
00272   Entry* sub_entry;
00273   for(sub_entry = m_entries.first(); sub_entry; sub_entry = m_entries.next())
00274   {
00275      if (!sub_entry->dn_dirty)
00276      {
00277         sub_entry->dn_dirty = true;
00278         sub_entry->propagate_dirty();
00279      }
00280   }
00281 }
00282 
00283 #else // !HAVE_DNOTIFY
00284 // slots always have to be defined...
00285 void KDirWatchPrivate::slotActivated() {}
00286 #endif
00287 
00288 /* A KDirWatch instance is interested in getting events for
00289  * this file/Dir entry.
00290  */
00291 void KDirWatchPrivate::Entry::addClient(KDirWatch* instance)
00292 {
00293   Client* client = m_clients.first();
00294   for(;client; client = m_clients.next())
00295     if (client->instance == instance) break;
00296 
00297   if (client) {
00298     client->count++;
00299     return;
00300   }
00301 
00302   client = new Client;
00303   client->instance = instance;
00304   client->count = 1;
00305   client->watchingStopped = instance->isStopped();
00306   client->pending = NoChange;
00307 
00308   m_clients.append(client);
00309 }
00310 
00311 void KDirWatchPrivate::Entry::removeClient(KDirWatch* instance)
00312 {
00313   Client* client = m_clients.first();
00314   for(;client; client = m_clients.next())
00315     if (client->instance == instance) break;
00316 
00317   if (client) {
00318     client->count--;
00319     if (client->count == 0) {
00320       m_clients.removeRef(client);
00321       delete client;
00322     }
00323   }
00324 }
00325 
00326 /* get number of clients */
00327 int KDirWatchPrivate::Entry::clients()
00328 {
00329   int clients = 0;
00330   Client* client = m_clients.first();
00331   for(;client; client = m_clients.next())
00332     clients += client->count;
00333 
00334   return clients;
00335 }
00336 
00337 
00338 KDirWatchPrivate::Entry* KDirWatchPrivate::entry(const QString& _path)
00339 {
00340 // we only support absolute paths
00341   if (_path.left(1) != "/") {
00342     return 0;
00343   }
00344 
00345   QString path = _path;
00346 
00347   if ( path.length() > 1 && path.right(1) == "/" )
00348     path.truncate( path.length() - 1 );
00349 
00350   EntryMap::Iterator it = m_mapEntries.find( path );
00351   if ( it == m_mapEntries.end() )
00352     return 0;
00353   else
00354     return &(*it);
00355 }
00356 
00357 // set polling frequency for a entry and adjust global freq if needed
00358 void KDirWatchPrivate::useFreq(Entry* e, int newFreq)
00359 {
00360   e->freq = newFreq;
00361 
00362   // a reasonable frequency for the global polling timer
00363   if (e->freq < freq) {
00364     freq = e->freq;
00365     if (timer->isActive()) timer->changeInterval(freq);
00366     kdDebug(7001) << "Global Poll Freq is now " << freq << " msec" << endl;
00367   }
00368 }
00369 
00370 
00371 #if defined(HAVE_FAM)
00372 // setup FAM notification, returns false if not possible
00373 bool KDirWatchPrivate::useFAM(Entry* e)
00374 {
00375   if (!use_fam) return false;
00376 
00377   e->m_mode = FAMMode;
00378 
00379   if (e->isDir) {
00380     if (e->m_status == NonExistent) {
00381       // If the directory does not exist we watch the parent directory
00382       addEntry(0, QDir::cleanDirPath(e->path+"/.."), e, true);
00383     }
00384     else {
00385       int res =FAMMonitorDirectory(&fc, QFile::encodeName(e->path),
00386                    &(e->fr), e);
00387       if (res<0) {
00388     e->m_mode = UnknownMode;
00389     use_fam=false;
00390     return false;
00391       }
00392       kdDebug(7001) << " Setup FAM (Req "
00393             << FAMREQUEST_GETREQNUM(&(e->fr))
00394             << ") for " << e->path << endl;
00395     }
00396   }
00397   else {
00398     if (e->m_status == NonExistent) {
00399       // If the file does not exist we watch the directory
00400       addEntry(0, QFileInfo(e->path).dirPath(true), e, true);
00401     }
00402     else {
00403       int res = FAMMonitorFile(&fc, QFile::encodeName(e->path),
00404                    &(e->fr), e);
00405       if (res<0) {
00406     e->m_mode = UnknownMode;
00407     use_fam=false;
00408     return false;
00409       }
00410 
00411       kdDebug(7001) << " Setup FAM (Req "
00412             << FAMREQUEST_GETREQNUM(&(e->fr))
00413             << ") for " << e->path << endl;
00414     }
00415   }
00416 
00417   // handle FAM events to avoid deadlock
00418   // (FAM sends back all files in a directory when monitoring)
00419   famEventReceived();
00420 
00421   return true;
00422 }
00423 #endif
00424 
00425 
00426 #ifdef HAVE_DNOTIFY
00427 // setup DNotify notification, returns false if not possible
00428 bool KDirWatchPrivate::useDNotify(Entry* e)
00429 {
00430   e->dn_fd = 0;
00431   if (!supports_dnotify) return false;
00432 
00433   e->m_mode = DNotifyMode;
00434 
00435   if (e->isDir) {
00436     e->dn_dirty = false;
00437     if (e->m_status == Normal) {
00438       int fd = open(QFile::encodeName(e->path).data(), O_RDONLY);
00439       if (fd<0) {
00440     e->m_mode = UnknownMode;
00441     return false;
00442       }
00443 
00444       int mask = DN_DELETE|DN_CREATE|DN_RENAME|DN_MULTISHOT;
00445       // if dependant is a file watch, we check for MODIFY & ATTRIB too
00446       for(Entry* dep=e->m_entries.first();dep;dep=e->m_entries.next())
00447     if (!dep->isDir) { mask |= DN_MODIFY|DN_ATTRIB; break; }
00448 
00449       if(fcntl(fd, F_SETSIG, dnotify_signal) < 0 ||
00450      fcntl(fd, F_NOTIFY, mask) < 0) {
00451 
00452     kdDebug(7001) << "Not using Linux Directory Notifications."
00453               << endl;
00454     supports_dnotify = false;
00455     ::close(fd);
00456     e->m_mode = UnknownMode;
00457     return false;
00458       }
00459 
00460       fd_Entry.replace(fd, e);
00461       e->dn_fd = fd;
00462 
00463       kdDebug(7001) << " Setup DNotify (fd " << fd
00464             << ") for " << e->path << endl;
00465     }
00466     else { // NotExisting
00467       addEntry(0, QDir::cleanDirPath(e->path+"/.."), e, true);
00468     }
00469   }
00470   else { // File
00471     // we always watch the directory (DNOTIFY can't watch files alone)
00472     // this notifies us about changes of files therein
00473     addEntry(0, QFileInfo(e->path).dirPath(true), e, true);
00474   }
00475 
00476   return true;
00477 }
00478 #endif
00479 
00480 
00481 bool KDirWatchPrivate::useStat(Entry* e)
00482 {
00483   if (KIO::probably_slow_mounted(e->path))
00484     useFreq(e, m_nfsPollInterval);
00485   else
00486     useFreq(e, m_PollInterval);
00487 
00488   if (e->m_mode != StatMode) {
00489     e->m_mode = StatMode;
00490     statEntries++;
00491 
00492     if ( statEntries == 1 ) {
00493       // if this was first STAT entry (=timer was stopped)
00494       timer->start(freq);      // then start the timer
00495       kdDebug(7001) << " Started Polling Timer, freq " << freq << endl;
00496     }
00497   }
00498 
00499   kdDebug(7001) << " Setup Stat (freq " << e->freq
00500         << ") for " << e->path << endl;
00501 
00502   return true;
00503 }
00504 
00505 
00506 /* If <instance> !=0, this KDirWatch instance wants to watch at <_path>,
00507  * providing in <isDir> the type of the entry to be watched.
00508  * Sometimes, entries are dependant on each other: if <sub_entry> !=0,
00509  * this entry needs another entry to watch himself (when notExistent).
00510  */
00511 void KDirWatchPrivate::addEntry(KDirWatch* instance, const QString& _path,
00512                 Entry* sub_entry, bool isDir)
00513 {
00514   QString path = _path;
00515   if (path.startsWith("/dev/") || (path == "/dev"))
00516     return; // Don't even go there.
00517 
00518   if ( path.length() > 1 && path.right(1) == "/" )
00519     path.truncate( path.length() - 1 );
00520 
00521   EntryMap::Iterator it = m_mapEntries.find( path );
00522   if ( it != m_mapEntries.end() )
00523   {
00524     if (sub_entry) {
00525        (*it).m_entries.append(sub_entry);
00526        kdDebug(7001) << "Added already watched Entry " << path
00527              << " (for " << sub_entry->path << ")" << endl;
00528 #ifdef HAVE_DNOTIFY
00529        Entry* e = &(*it);
00530        if( e->dn_fd > 0 ) {
00531          int mask = DN_DELETE|DN_CREATE|DN_RENAME|DN_MULTISHOT;
00532          // if dependant is a file watch, we check for MODIFY & ATTRIB too
00533          for(Entry* dep=e->m_entries.first();dep;dep=e->m_entries.next())
00534            if (!dep->isDir) { mask |= DN_MODIFY|DN_ATTRIB; break; }
00535      if( fcntl(e->dn_fd, F_NOTIFY, mask) < 0) { // shouldn't happen
00536        ::close(e->dn_fd);
00537        e->m_mode = UnknownMode;
00538        fd_Entry.remove(e->dn_fd);
00539            e->dn_fd = 0;
00540            useStat( e );
00541          }
00542        }
00543 #endif
00544     }
00545     else {
00546        (*it).addClient(instance);
00547        kdDebug(7001) << "Added already watched Entry " << path
00548              << " (now " <<  (*it).clients() << " clients)"
00549              << QString(" [%1]").arg(instance->name()) << endl;
00550     }
00551     return;
00552   }
00553 
00554   // we have a new path to watch
00555 
00556   QFileInfo info(path);
00557 
00558   Entry newEntry;
00559   m_mapEntries.insert( path, newEntry );
00560   // the insert does a copy, so we have to use <e> now
00561   Entry* e = &(m_mapEntries[path]);
00562 
00563   if (info.exists()) {
00564     e->isDir = info.isDir();
00565 
00566     if (e->isDir && !isDir)
00567       qWarning("KDirWatch: %s is a directory. Use addDir!", path.ascii());
00568     else if (!e->isDir && isDir)
00569       qWarning("KDirWatch: %s is a file. Use addFile!", path.ascii());
00570 
00571     e->m_ctime = info.lastModified();
00572     e->m_status = Normal;
00573   }
00574   else {
00575     e->isDir = isDir;
00576     e->m_ctime = QDateTime(); // invalid
00577     e->m_status = NonExistent;
00578   }
00579 
00580   e->path = path;
00581   if (sub_entry)
00582      e->m_entries.append(sub_entry);
00583   else
00584     e->addClient(instance);
00585 
00586   kdDebug(7001) << "Added " << (e->isDir ? "Dir ":"File ") << path
00587         << (e->m_status == NonExistent ? " NotExisting" : "")
00588         << (sub_entry ? QString(" for %1").arg(sub_entry->path) : QString(""))
00589         << (instance ? QString(" [%1]").arg(instance->name()) : QString(""))
00590         << endl;
00591 
00592 
00593   // now setup the notification method
00594   e->m_mode = UnknownMode;
00595   e->msecLeft = 0;
00596 
00597 #if defined(HAVE_FAM)
00598   if (useFAM(e)) return;
00599 #endif
00600 
00601 #ifdef HAVE_DNOTIFY
00602   if (useDNotify(e)) return;
00603 #endif
00604 
00605   useStat(e);
00606 }
00607 
00608 
00609 void KDirWatchPrivate::removeEntry( KDirWatch* instance,
00610                     const QString& _path, Entry* sub_entry )
00611 {
00612   Entry* e = entry(_path);
00613   if (!e) {
00614     kdWarning(7001) << "KDirWatch::removeDir can't handle '" << _path << "'" << endl;
00615     return;
00616   }
00617 
00618   if (sub_entry)
00619     e->m_entries.removeRef(sub_entry);
00620   else
00621     e->removeClient(instance);
00622 
00623   if (e->m_clients.count() || e->m_entries.count())
00624     return;
00625 
00626   if (delayRemove) {
00627     removeList.append(e);
00628     // now e->isValid() is false
00629     return;
00630   }
00631 
00632 #ifdef HAVE_FAM
00633   if (e->m_mode == FAMMode) {
00634     if ( e->m_status == Normal) {
00635       FAMCancelMonitor(&fc, &(e->fr) );
00636       kdDebug(7001) << "Cancelled FAM (Req "
00637             << FAMREQUEST_GETREQNUM(&(e->fr))
00638             << ") for " << e->path << endl;
00639     }
00640     else {
00641       if (e->isDir)
00642     removeEntry(0, QDir::cleanDirPath(e->path+"/.."), e);
00643       else
00644     removeEntry(0, QFileInfo(e->path).dirPath(true), e);
00645     }
00646   }
00647 #endif
00648 
00649 #ifdef HAVE_DNOTIFY
00650   if (e->m_mode == DNotifyMode) {
00651     if (!e->isDir) {
00652       removeEntry(0, QFileInfo(e->path).dirPath(true), e);
00653     }
00654     else { // isDir
00655       // must close the FD.
00656       if ( e->m_status == Normal) {
00657     if (e->dn_fd) {
00658       ::close(e->dn_fd);
00659       fd_Entry.remove(e->dn_fd);
00660 
00661       kdDebug(7001) << "Cancelled DNotify (fd " << e->dn_fd
00662             << ") for " << e->path << endl;
00663       e->dn_fd = 0;
00664 
00665     }
00666       }
00667       else {
00668     removeEntry(0, QDir::cleanDirPath(e->path+"/.."), e);
00669       }
00670     }
00671   }
00672 #endif
00673 
00674   if (e->m_mode == StatMode) {
00675     statEntries--;
00676     if ( statEntries == 0 ) {
00677       timer->stop(); // stop timer if lists are empty
00678       kdDebug(7001) << " Stopped Polling Timer" << endl;
00679     }
00680   }
00681 
00682   kdDebug(7001) << "Removed " << (e->isDir ? "Dir ":"File ") << e->path
00683         << (sub_entry ? QString(" for %1").arg(sub_entry->path) : QString(""))
00684         << (instance ? QString(" [%1]").arg(instance->name()) : QString(""))
00685         << endl;
00686   m_mapEntries.remove( e->path ); // <e> not valid any more
00687 }
00688 
00689 
00690 /* Called from KDirWatch destructor:
00691  * remove <instance> as client from all entries
00692  */
00693 void KDirWatchPrivate::removeEntries( KDirWatch* instance )
00694 {
00695   QPtrList<Entry> list;
00696   int minfreq = 3600000;
00697 
00698   // put all entries where instance is a client in list
00699   EntryMap::Iterator it = m_mapEntries.begin();
00700   for( ; it != m_mapEntries.end(); ++it ) {
00701     Client* c = (*it).m_clients.first();
00702     for(;c;c=(*it).m_clients.next())
00703       if (c->instance == instance) break;
00704     if (c) {
00705       c->count = 1; // forces deletion of instance as client
00706       list.append(&(*it));
00707     }
00708     else if ( (*it).m_mode == StatMode && (*it).freq < minfreq )
00709       minfreq = (*it).freq;
00710   }
00711 
00712   for(Entry* e=list.first();e;e=list.next())
00713     removeEntry(instance, e->path, 0);
00714 
00715   if (minfreq > freq) {
00716     // we can decrease the global polling frequency
00717     freq = minfreq;
00718     if (timer->isActive()) timer->changeInterval(freq);
00719     kdDebug(7001) << "Poll Freq now " << freq << " msec" << endl;
00720   }
00721 }
00722 
00723 // instance ==0: stop scanning for all instances
00724 bool KDirWatchPrivate::stopEntryScan( KDirWatch* instance, Entry* e)
00725 {
00726   int stillWatching = 0;
00727   Client* c = e->m_clients.first();
00728   for(;c;c=e->m_clients.next()) {
00729     if (!instance || instance == c->instance)
00730       c->watchingStopped = true;
00731     else if (!c->watchingStopped)
00732       stillWatching += c->count;
00733   }
00734 
00735   kdDebug(7001) << instance->name() << " stopped scanning " << e->path
00736         << " (now " << stillWatching << " watchers)" << endl;
00737 
00738   if (stillWatching == 0) {
00739     // if nobody is interested, we don't watch
00740     e->m_ctime = QDateTime(); // invalid
00741     //    e->m_status = Normal;
00742   }
00743   return true;
00744 }
00745 
00746 // instance ==0: start scanning for all instances
00747 bool KDirWatchPrivate::restartEntryScan( KDirWatch* instance, Entry* e,
00748                      bool notify)
00749 {
00750   int wasWatching = 0, newWatching = 0;
00751   Client* c = e->m_clients.first();
00752   for(;c;c=e->m_clients.next()) {
00753     if (!c->watchingStopped)
00754       wasWatching += c->count;
00755     else if (!instance || instance == c->instance) {
00756       c->watchingStopped = false;
00757       newWatching += c->count;
00758     }
00759   }
00760   if (newWatching == 0)
00761     return false;
00762 
00763   kdDebug(7001) << instance->name() << " restarted scanning " << e->path
00764         << " (now " << wasWatching+newWatching << " watchers)" << endl;
00765 
00766   // restart watching and emit pending events
00767 
00768   int ev = NoChange;
00769   if (wasWatching == 0) {
00770     if (!notify) {
00771       QFileInfo info(e->path);
00772       if (info.exists()) {
00773     e->m_ctime = info.lastModified();
00774     e->m_status = Normal;
00775       }
00776       else {
00777     e->m_ctime = QDateTime(); // invalid
00778     e->m_status = NonExistent;
00779       }
00780     }
00781     e->msecLeft = 0;
00782     ev = scanEntry(e);
00783   }
00784   emitEvent(e,ev);
00785 
00786   return true;
00787 }
00788 
00789 // instance ==0: stop scanning for all instances
00790 void KDirWatchPrivate::stopScan(KDirWatch* instance)
00791 {
00792   EntryMap::Iterator it = m_mapEntries.begin();
00793   for( ; it != m_mapEntries.end(); ++it )
00794     stopEntryScan(instance, &(*it));
00795 }
00796 
00797 
00798 void KDirWatchPrivate::startScan(KDirWatch* instance,
00799                  bool notify, bool skippedToo )
00800 {
00801   if (!notify)
00802     resetList(instance,skippedToo);
00803 
00804   EntryMap::Iterator it = m_mapEntries.begin();
00805   for( ; it != m_mapEntries.end(); ++it )
00806     restartEntryScan(instance, &(*it), notify);
00807 
00808   // timer should still be running when in polling mode
00809 }
00810 
00811 
00812 // clear all pending events, also from stopped
00813 void KDirWatchPrivate::resetList( KDirWatch* /*instance*/,
00814                   bool skippedToo )
00815 {
00816   EntryMap::Iterator it = m_mapEntries.begin();
00817   for( ; it != m_mapEntries.end(); ++it ) {
00818 
00819     Client* c = (*it).m_clients.first();
00820     for(;c;c=(*it).m_clients.next())
00821       if (!c->watchingStopped || skippedToo)
00822     c->pending = NoChange;
00823   }
00824 }
00825 
00826 // Return event happened on <e>
00827 //
00828 int KDirWatchPrivate::scanEntry(Entry* e)
00829 {
00830 #ifdef HAVE_FAM
00831   // we do not stat entries using FAM
00832   if (e->m_mode == FAMMode) return NoChange;
00833 #endif
00834 
00835   // Shouldn't happen: Ignore "unknown" notification method
00836   if (e->m_mode == UnknownMode) return NoChange;
00837 
00838 #ifdef HAVE_DNOTIFY
00839   if (e->m_mode == DNotifyMode) {
00840     // we know nothing has changed, no need to stat
00841     if(!e->dn_dirty) return NoChange;
00842     e->dn_dirty = false;
00843   }
00844 #endif
00845 
00846   if (e->m_mode == StatMode) {
00847     // only scan if timeout on entry timer happens;
00848     // e.g. when using 500msec global timer, a entry
00849     // with freq=5000 is only watched every 10th time
00850 
00851     e->msecLeft -= freq;
00852     if (e->msecLeft>0) return NoChange;
00853     e->msecLeft += e->freq;
00854   }
00855 
00856   QFileInfo info(e->path);
00857   if (info.exists()) {
00858 
00859     if (e->m_status == NonExistent) {
00860       e->m_ctime = info.lastModified();
00861       e->m_status = Normal;
00862       return Created;
00863     }
00864 
00865     if ( e->m_ctime.isValid() &&
00866      (info.lastModified() != e->m_ctime) ) {
00867       e->m_ctime = info.lastModified();
00868       return Changed;
00869     }
00870 
00871     return NoChange;
00872   }
00873 
00874   // dir/file doesn't exist
00875 
00876   if (!e->m_ctime.isValid())
00877     return NoChange;
00878 
00879   e->m_ctime = QDateTime(); // invalid
00880   e->m_status = NonExistent;
00881 
00882   return Deleted;
00883 }
00884 
00885 /* Notify all interested KDirWatch instances about a given event on an entry
00886  * and stored pending events. When watching is stopped, the event is
00887  * added to the pending events.
00888  */
00889 void KDirWatchPrivate::emitEvent(Entry* e, int event, const QString &fileName)
00890 {
00891   QString path = e->path;
00892   if (!fileName.isEmpty()) {
00893     if (fileName[0] == '/')
00894       path = fileName;
00895     else
00896       path += "/" + fileName;
00897   }
00898 
00899   Client* c = e->m_clients.first();
00900   for(;c;c=e->m_clients.next()) {
00901     if (c->instance==0 || c->count==0) continue;
00902 
00903     if (c->watchingStopped) {
00904       // add event to pending...
00905       if (event == Changed)
00906     c->pending |= event;
00907       else if (event == Created || event == Deleted)
00908     c->pending = event;
00909       continue;
00910     }
00911     // not stopped
00912     if (event == NoChange || event == Changed)
00913       event |= c->pending;
00914     c->pending = NoChange;
00915     if (event == NoChange) continue;
00916 
00917     if (event & Deleted) {
00918       c->instance->setDeleted(path);
00919       // emit only Deleted event...
00920       continue;
00921     }
00922 
00923     if (event & Created) {
00924       c->instance->setCreated(path);
00925       // possible emit Change event after creation
00926     }
00927 
00928     if (event & Changed)
00929       c->instance->setDirty(path);
00930   }
00931 }
00932 
00933 // Remove entries which were marked to be removed
00934 void KDirWatchPrivate::slotRemoveDelayed()
00935 {
00936   Entry* e;
00937   delayRemove = false;
00938   for(e=removeList.first();e;e=removeList.next())
00939     removeEntry(0, e->path, 0);
00940   removeList.clear();
00941 }
00942 
00943 /* Scan all entries to be watched for changes. This is done regularly
00944  * when polling and once after a DNOTIFY signal. This is NOT used by FAM.
00945  */
00946 void KDirWatchPrivate::slotRescan()
00947 {
00948   EntryMap::Iterator it;
00949 
00950   // People can do very long things in the slot connected to dirty(),
00951   // like showing a message box. We don't want to keep polling during
00952   // that time, otherwise the value of 'delayRemove' will be reset.
00953   bool timerRunning = timer->isActive();
00954   if ( timerRunning )
00955     timer->stop();
00956 
00957   // We delay deletions of entries this way.
00958   // removeDir(), when called in slotDirty(), can cause a crash otherwise
00959   delayRemove = true;
00960 
00961 #ifdef HAVE_DNOTIFY
00962   QPtrList<Entry> dList, cList;
00963 
00964   // for DNotify method,
00965   if (rescan_all)
00966   {
00967     // mark all as dirty
00968     it = m_mapEntries.begin();
00969     for( ; it != m_mapEntries.end(); ++it )
00970       (*it).dn_dirty = true;
00971     rescan_all = false;
00972   }
00973   else
00974   {
00975     // progate dirty flag to dependant entries (e.g. file watches)
00976     it = m_mapEntries.begin();
00977     for( ; it != m_mapEntries.end(); ++it )
00978       if ( ((*it).m_mode == DNotifyMode) && (*it).dn_dirty )
00979         (*it).propagate_dirty();
00980   }
00981 
00982 #endif
00983 
00984   it = m_mapEntries.begin();
00985   for( ; it != m_mapEntries.end(); ++it ) {
00986     // we don't check invalid entries (i.e. remove delayed)
00987     if (!(*it).isValid()) continue;
00988 
00989     int ev = scanEntry( &(*it) );
00990 
00991 #ifdef HAVE_DNOTIFY
00992     if ((*it).m_mode == DNotifyMode) {
00993       if ((*it).isDir && (ev == Deleted)) {
00994     dList.append( &(*it) );
00995 
00996     // must close the FD.
00997     if ((*it).dn_fd) {
00998       ::close((*it).dn_fd);
00999       fd_Entry.remove((*it).dn_fd);
01000       (*it).dn_fd = 0;
01001     }
01002       }
01003 
01004       else if ((*it).isDir && (ev == Created)) {
01005     // For created, but yet without DNOTIFYing ...
01006     if ( (*it).dn_fd == 0) {
01007       cList.append( &(*it) );
01008       if (! useDNotify( &(*it) )) {
01009         // if DNotify setup fails...
01010         useStat( &(*it) );
01011       }
01012     }
01013       }
01014     }
01015 #endif
01016 
01017     if ( ev != NoChange )
01018       emitEvent( &(*it), ev);
01019   }
01020 
01021 
01022 #ifdef HAVE_DNOTIFY
01023   // Scan parent of deleted directories for new creation
01024   Entry* e;
01025   for(e=dList.first();e;e=dList.next())
01026     addEntry(0, QDir::cleanDirPath( e->path+"/.."), e, true);
01027 
01028   // Remove watch of parent of new created directories
01029   for(e=cList.first();e;e=cList.next())
01030     removeEntry(0, QDir::cleanDirPath( e->path+"/.."), e);
01031 #endif
01032 
01033   if ( timerRunning )
01034     timer->start(freq);
01035 
01036   QTimer::singleShot(0, this, SLOT(slotRemoveDelayed()));
01037 }
01038 
01039 #ifdef HAVE_FAM
01040 void KDirWatchPrivate::famEventReceived()
01041 {
01042   static FAMEvent fe;
01043 
01044   delayRemove = true;
01045 
01046   while(use_fam && FAMPending(&fc)) {
01047     if (FAMNextEvent(&fc, &fe) == -1) {
01048       kdWarning(7001) << "FAM connection problem, switching to polling."
01049               << endl;
01050       use_fam = false;
01051       delete sn; sn = 0;
01052 
01053       // Replace all FAMMode entries with DNotify/Stat
01054       EntryMap::Iterator it;
01055       it = m_mapEntries.begin();
01056       for( ; it != m_mapEntries.end(); ++it )
01057     if ((*it).m_mode == FAMMode && (*it).m_clients.count()>0) {
01058 #ifdef HAVE_DNOTIFY
01059       if (useDNotify( &(*it) )) continue;
01060 #endif
01061       useStat( &(*it) );
01062     }
01063     }
01064     else
01065       checkFAMEvent(&fe);
01066   }
01067 
01068   QTimer::singleShot(0, this, SLOT(slotRemoveDelayed()));
01069 }
01070 
01071 void KDirWatchPrivate::checkFAMEvent(FAMEvent* fe)
01072 {
01073   // Don't be too verbose ;-)
01074   if ((fe->code == FAMExists) ||
01075       (fe->code == FAMEndExist) ||
01076       (fe->code == FAMAcknowledge)) return;
01077 
01078   // $HOME/.X.err grows with debug output, so don't notify change
01079   if ( *(fe->filename) == '.') {
01080     if (strncmp(fe->filename, ".X.err", 6) == 0) return;
01081     if (strncmp(fe->filename, ".xsession-errors", 16) == 0) return;
01082   }
01083 
01084   Entry* e = 0;
01085   EntryMap::Iterator it = m_mapEntries.begin();
01086   for( ; it != m_mapEntries.end(); ++it )
01087     if (FAMREQUEST_GETREQNUM(&( (*it).fr )) ==
01088        FAMREQUEST_GETREQNUM(&(fe->fr)) ) {
01089       e = &(*it);
01090       break;
01091     }
01092 
01093   // Entry* e = static_cast<Entry*>(fe->userdata);
01094 
01095   kdDebug(7001) << "Processing FAM event ("
01096         << ((fe->code == FAMChanged) ? "FAMChanged" :
01097             (fe->code == FAMDeleted) ? "FAMDeleted" :
01098             (fe->code == FAMStartExecuting) ? "FAMStartExecuting" :
01099             (fe->code == FAMStopExecuting) ? "FAMStopExecuting" :
01100             (fe->code == FAMCreated) ? "FAMCreated" :
01101             (fe->code == FAMMoved) ? "FAMMoved" :
01102             (fe->code == FAMAcknowledge) ? "FAMAcknowledge" :
01103             (fe->code == FAMExists) ? "FAMExists" :
01104             (fe->code == FAMEndExist) ? "FAMEndExist" : "Unknown Code")
01105         << ", " << fe->filename
01106         << ", Req " << FAMREQUEST_GETREQNUM(&(fe->fr))
01107         << ")" << endl;
01108 
01109   if (!e) {
01110     // this happens e.g. for FAMAcknowledge after deleting a dir...
01111     //    kdDebug(7001) << "No entry for FAM event ?!" << endl;
01112     return;
01113   }
01114 
01115   if (e->m_status == NonExistent) {
01116     kdDebug(7001) << "FAM event for nonExistent entry " << e->path << endl;
01117     return;
01118   }
01119 
01120   if (e->isDir)
01121     switch (fe->code)
01122     {
01123       case FAMDeleted:
01124        // file absolute: watched dir
01125         if (fe->filename[0] == '/')
01126         {
01127           // a watched directory was deleted
01128 
01129           e->m_status = NonExistent;
01130           FAMCancelMonitor(&fc, &(e->fr) ); // needed ?
01131           kdDebug(7001) << "Cancelled FAMReq "
01132                         << FAMREQUEST_GETREQNUM(&(e->fr))
01133                         << " for " << e->path << endl;
01134           // Scan parent for a new creation
01135           addEntry(0, QDir::cleanDirPath( e->path+"/.."), e, true);
01136         }
01137         emitEvent(e, Deleted, QFile::decodeName(fe->filename));
01138         break;
01139 
01140       case FAMCreated: {
01141           // check for creation of a directory we have to watch
01142           Entry *sub_entry = e->m_entries.first();
01143           for(;sub_entry; sub_entry = e->m_entries.next())
01144             if (sub_entry->path == e->path + "/" + fe->filename) break;
01145           if (sub_entry && sub_entry->isDir) {
01146             QString path = e->path;
01147             removeEntry(0,e->path,sub_entry); // <e> can be invalid here!!
01148             sub_entry->m_status = Normal;
01149             if (!useFAM(sub_entry))
01150               useStat(sub_entry);
01151 
01152             emitEvent(sub_entry, Created);
01153           }
01154           else emitEvent(e, Created, QFile::decodeName(fe->filename));
01155           break;
01156         }
01157 
01158       case FAMChanged:
01159         emitEvent(e, Changed, QFile::decodeName(fe->filename));
01160 
01161       default:
01162         break;
01163     }
01164   else switch (fe->code)
01165     {
01166       case FAMCreated: emitEvent(e, Created);
01167                        break;
01168       case FAMDeleted: emitEvent(e, Deleted);
01169                        break;
01170       case FAMChanged: emitEvent(e, Changed);
01171                        break;
01172       default: break;
01173     }
01174 }
01175 #else
01176 void KDirWatchPrivate::famEventReceived() {}
01177 #endif
01178 
01179 
01180 void KDirWatchPrivate::statistics()
01181 {
01182   EntryMap::Iterator it;
01183 
01184   kdDebug(7001) << "Entries watched:" << endl;
01185   if (m_mapEntries.count()==0) {
01186     kdDebug(7001) << "  None." << endl;
01187   }
01188   else {
01189     it = m_mapEntries.begin();
01190     for( ; it != m_mapEntries.end(); ++it ) {
01191       Entry* e = &(*it);
01192       kdDebug(7001) << "  " << e->path << " ("
01193             << ((e->m_status==Normal)?"":"Nonexistent ")
01194             << (e->isDir ? "Dir":"File") << ", using "
01195             << ((e->m_mode == FAMMode) ? "FAM" :
01196             (e->m_mode == DNotifyMode) ? "DNotify" :
01197             (e->m_mode == StatMode) ? "Stat" : "Unknown Method")
01198             << ")" << endl;
01199 
01200       Client* c = e->m_clients.first();
01201       for(;c; c = e->m_clients.next()) {
01202     QString pending;
01203     if (c->watchingStopped) {
01204       if (c->pending & Deleted) pending += "deleted ";
01205       if (c->pending & Created) pending += "created ";
01206       if (c->pending & Changed) pending += "changed ";
01207       if (!pending.isEmpty()) pending = " (pending: " + pending + ")";
01208       pending = ", stopped" + pending;
01209     }
01210     kdDebug(7001) << "    by " << c->instance->name()
01211               << " (" << c->count << " times)"
01212               << pending << endl;
01213       }
01214       if (e->m_entries.count()>0) {
01215     kdDebug(7001) << "    dependent entries:" << endl;
01216     Entry* d = e->m_entries.first();
01217     for(;d; d = e->m_entries.next()) {
01218       kdDebug(7001) << "      " << d->path << endl;
01219     }
01220       }
01221     }
01222   }
01223 }
01224 
01225 
01226 //
01227 // Class KDirWatch
01228 //
01229 
01230 static KStaticDeleter<KDirWatch> sd_dw;
01231 KDirWatch* KDirWatch::s_pSelf = 0L;
01232 
01233 KDirWatch* KDirWatch::self()
01234 {
01235   if ( !s_pSelf ) {
01236     sd_dw.setObject( s_pSelf, new KDirWatch );
01237   }
01238 
01239   return s_pSelf;
01240 }
01241 
01242 bool KDirWatch::exists()
01243 {
01244   return s_pSelf != 0;
01245 }
01246 
01247 KDirWatch::KDirWatch (QObject* parent, const char* name)
01248   : QObject(parent,name)
01249 {
01250   if (!name) {
01251     static int nameCounter = 0;
01252 
01253     nameCounter++;
01254     setName(QString("KDirWatch-%1").arg(nameCounter).ascii());
01255   }
01256 
01257   if (!dwp_self)
01258     dwp_self = new KDirWatchPrivate;
01259   d = dwp_self;
01260   d->ref();
01261 
01262   _isStopped = false;
01263 }
01264 
01265 KDirWatch::~KDirWatch()
01266 {
01267   if (d) d->removeEntries(this);
01268   if ( d->deref() )
01269   {
01270     // delete it if it's the last one
01271     delete d;
01272     dwp_self = 0L;
01273   }
01274 }
01275 
01276 
01277 // TODO: add watchFiles/recursive support
01278 void KDirWatch::addDir( const QString& _path,
01279             bool watchFiles, bool recursive)
01280 {
01281   if (watchFiles || recursive) {
01282     kdDebug(7001) << "addDir - recursive/watchFiles not supported in KDE 3.0"
01283           << endl;
01284   }
01285   if (d) d->addEntry(this, _path, 0, true);
01286 }
01287 
01288 void KDirWatch::addFile( const QString& _path )
01289 {
01290   if (d) d->addEntry(this, _path, 0, false);
01291 }
01292 
01293 QDateTime KDirWatch::ctime( const QString &_path )
01294 {
01295   KDirWatchPrivate::Entry* e = d->entry(_path);
01296 
01297   if (!e)
01298     return QDateTime();
01299   else
01300     return e->m_ctime;
01301 }
01302 
01303 void KDirWatch::removeDir( const QString& _path )
01304 {
01305   if (d) d->removeEntry(this, _path, 0);
01306 }
01307 
01308 void KDirWatch::removeFile( const QString& _path )
01309 {
01310   if (d) d->removeEntry(this, _path, 0);
01311 }
01312 
01313 bool KDirWatch::stopDirScan( const QString& _path )
01314 {
01315   if (d) {
01316     KDirWatchPrivate::Entry *e = d->entry(_path);
01317     if (e && e->isDir) return d->stopEntryScan(this, e);
01318   }
01319   return false;
01320 }
01321 
01322 bool KDirWatch::restartDirScan( const QString& _path )
01323 {
01324   if (d) {
01325     KDirWatchPrivate::Entry *e = d->entry(_path);
01326     if (e && e->isDir)
01327       // restart without notifying pending events
01328       return d->restartEntryScan(this, e, false);
01329   }
01330   return false;
01331 }
01332 
01333 void KDirWatch::stopScan()
01334 {
01335   if (d) d->stopScan(this);
01336   _isStopped = true;
01337 }
01338 
01339 void KDirWatch::startScan( bool notify, bool skippedToo )
01340 {
01341   _isStopped = false;
01342   if (d) d->startScan(this, notify, skippedToo);
01343 }
01344 
01345 
01346 bool KDirWatch::contains( const QString& _path ) const
01347 {
01348   KDirWatchPrivate::Entry* e = d->entry(_path);
01349   if (!e)
01350      return false;
01351 
01352   KDirWatchPrivate::Client* c = e->m_clients.first();
01353   for(;c;c=e->m_clients.next())
01354     if (c->instance == this) return true;
01355 
01356   return false;
01357 }
01358 
01359 void KDirWatch::statistics()
01360 {
01361   if (!dwp_self) {
01362     kdDebug(7001) << "KDirWatch not used" << endl;
01363     return;
01364   }
01365   dwp_self->statistics();
01366 }
01367 
01368 
01369 void KDirWatch::setCreated( const QString & _file )
01370 {
01371   kdDebug(7001) << name() << " emitting created " << _file << endl;
01372   emit created( _file );
01373 }
01374 
01375 void KDirWatch::setDirty( const QString & _file )
01376 {
01377   kdDebug(7001) << name() << " emitting dirty " << _file << endl;
01378   emit dirty( _file );
01379 }
01380 
01381 void KDirWatch::setDeleted( const QString & _file )
01382 {
01383   kdDebug(7001) << name() << " emitting deleted " << _file << endl;
01384   emit deleted( _file );
01385 }
01386 
01387 #include "kdirwatch.moc"
01388 #include "kdirwatch_p.moc"
01389 
01390 //sven
01391 
01392 // vim: sw=2 ts=8 et
KDE Logo
This file is part of the documentation for kdelibs Version 3.1.0.
Documentation copyright © 1996-2002 the KDE developers.
Generated on Wed Oct 8 12:21:29 2003 by doxygen 1.2.18 written by Dimitri van Heesch, © 1997-2001