• Skip to content
  • Skip to link menu
KDE 4.3 API Reference
  • KDE API Reference
  • kdelibs
  • Sitemap
  • Contact Us
 

KInit

klauncher.cpp

Go to the documentation of this file.
00001 /*
00002   This file is part of the KDE libraries
00003   Copyright (c) 1999 Waldo Bastian <bastian@kde.org>
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., 51 Franklin Street, Fifth Floor,
00017   Boston, MA 02110-1301, USA.
00018 */
00019 
00020 #define QT_NO_CAST_FROM_ASCII
00021 
00022 #include "klauncher.h"
00023 #include "klauncher_cmds.h"
00024 #include "klauncher_adaptor.h"
00025 
00026 #include <config.h>
00027 
00028 #include <stdio.h>
00029 #include <unistd.h>
00030 #include <stdlib.h>
00031 #include <errno.h>
00032 #include <signal.h>
00033 #include <sys/time.h>
00034 
00035 #ifdef Q_WS_X11
00036 #include <kstartupinfo.h>
00037 #include <X11/Xlib.h>
00038 #endif
00039 
00040 #include <QtCore/QFile>
00041 
00042 #include <kconfig.h>
00043 #include <kdebug.h>
00044 #include <kde_file.h>
00045 #include <klibloader.h>
00046 #include <klocale.h>
00047 #include <kprotocolmanager.h>
00048 #include <kprotocolinfo.h>
00049 #include <krun.h>
00050 #include <kstandarddirs.h>
00051 #include <ktemporaryfile.h>
00052 #include <kdesktopfile.h>
00053 #include <kurl.h>
00054 
00055 #include <kio/global.h>
00056 #include <kio/connection.h>
00057 #include <kio/slaveinterface.h>
00058 
00059 // Dispose slaves after being idle for SLAVE_MAX_IDLE seconds
00060 #define SLAVE_MAX_IDLE  30
00061 
00062 // #define KLAUNCHER_VERBOSE_OUTPUT
00063 
00064 static const char* const s_DBusStartupTypeToString[] =
00065     { "DBusNone", "DBusUnique", "DBusMulti", "DBusWait", "ERROR" };
00066 
00067 using namespace KIO;
00068 
00069 IdleSlave::IdleSlave(QObject *parent)
00070     : QObject(parent)
00071 {
00072    QObject::connect(&mConn, SIGNAL(readyRead()), this, SLOT(gotInput()));
00073    // Send it a SLAVE_STATUS command.
00074    mConn.send( CMD_SLAVE_STATUS );
00075    mPid = 0;
00076    mBirthDate = time(0);
00077    mOnHold = false;
00078 }
00079 
00080 template<int T> struct PIDType { typedef pid_t PID_t; } ;
00081 template<> struct PIDType<2> { typedef qint16 PID_t; } ;
00082 template<> struct PIDType<4> { typedef qint32 PID_t; } ;
00083 
00084 void
00085 IdleSlave::gotInput()
00086 {
00087    int cmd;
00088    QByteArray data;
00089    if (mConn.read( &cmd, data) == -1)
00090    {
00091       // Communication problem with slave.
00092       kError(7016) << "SlavePool: No communication with slave." << endl;
00093       deleteLater();
00094    }
00095    else if (cmd == MSG_SLAVE_ACK)
00096    {
00097       deleteLater();
00098    }
00099    else if (cmd != MSG_SLAVE_STATUS)
00100    {
00101       kError(7016) << "SlavePool: Unexpected data from slave." << endl;
00102       deleteLater();
00103    }
00104    else
00105    {
00106       QDataStream stream( data );
00107       PIDType<sizeof(pid_t)>::PID_t stream_pid;
00108       pid_t pid;
00109       QByteArray protocol;
00110       QString host;
00111       qint8 b;
00112       stream >> stream_pid >> protocol >> host >> b;
00113       pid = stream_pid;
00114 // Overload with (bool) onHold, (KUrl) url.
00115       if (!stream.atEnd())
00116       {
00117          KUrl url;
00118          stream >> url;
00119          mOnHold = true;
00120          mUrl = url;
00121       }
00122 
00123       mPid = pid;
00124       mConnected = (b != 0);
00125       mProtocol = QString::fromLatin1(protocol);
00126       mHost = host;
00127       emit statusUpdate(this);
00128    }
00129 }
00130 
00131 void
00132 IdleSlave::connect(const QString &app_socket)
00133 {
00134    QByteArray data;
00135    QDataStream stream( &data, QIODevice::WriteOnly);
00136    stream << app_socket;
00137    mConn.send( CMD_SLAVE_CONNECT, data );
00138    // Timeout!
00139 }
00140 
00141 void
00142 IdleSlave::reparseConfiguration()
00143 {
00144    mConn.send( CMD_REPARSECONFIGURATION );
00145 }
00146 
00147 bool
00148 IdleSlave::match(const QString &protocol, const QString &host, bool needConnected)
00149 {
00150    if (mOnHold || protocol != mProtocol) {
00151       return false;
00152    }
00153    if (host.isEmpty()) {
00154       return true;
00155    }
00156    return (host == mHost) && (!needConnected || mConnected);
00157 }
00158 
00159 bool
00160 IdleSlave::onHold(const KUrl &url)
00161 {
00162    if (!mOnHold) return false;
00163    return (url == mUrl);
00164 }
00165 
00166 int
00167 IdleSlave::age(time_t now)
00168 {
00169    return (int) difftime(now, mBirthDate);
00170 }
00171 
00172 static KLauncher* g_klauncher_self;
00173 
00174 #ifndef Q_WS_WIN
00175 KLauncher::KLauncher(int _kdeinitSocket)
00176   : QObject(0),
00177     kdeinitSocket(_kdeinitSocket)
00178 #else
00179 KLauncher::KLauncher()
00180   : QObject(0)
00181 #endif
00182 {
00183 #ifdef Q_WS_X11
00184    mCached_dpy = NULL;
00185 #endif
00186    Q_ASSERT( g_klauncher_self == NULL );
00187    g_klauncher_self = this;
00188 
00189    mAutoTimer.setSingleShot(true);
00190    new KLauncherAdaptor(this);
00191    QDBusConnection::sessionBus().registerObject(QLatin1String("/KLauncher"), this); // same as ktoolinvocation.cpp
00192 
00193    connect(&mAutoTimer, SIGNAL(timeout()), this, SLOT(slotAutoStart()));
00194    connect(QDBusConnection::sessionBus().interface(),
00195            SIGNAL(serviceOwnerChanged(QString,QString,QString)),
00196            SLOT(slotNameOwnerChanged(QString,QString,QString)));
00197 
00198    mConnectionServer.listenForRemote();
00199    connect(&mConnectionServer, SIGNAL(newConnection()), SLOT(acceptSlave()));
00200    if (!mConnectionServer.isListening())
00201    {
00202       // Severe error!
00203       qDebug("KLauncher: Fatal error, can't create tempfile!");
00204       ::_exit(1);
00205    }
00206 
00207    connect(&mTimer, SIGNAL(timeout()), SLOT(idleTimeout()));
00208 
00209 #ifndef Q_WS_WIN
00210    kdeinitNotifier = new QSocketNotifier(kdeinitSocket, QSocketNotifier::Read);
00211    connect(kdeinitNotifier, SIGNAL( activated( int )),
00212            this, SLOT( slotKDEInitData( int )));
00213    kdeinitNotifier->setEnabled( true );
00214 #endif
00215    lastRequest = 0;
00216    bProcessingQueue = false;
00217 
00218    mSlaveDebug = QString::fromLocal8Bit(qgetenv("KDE_SLAVE_DEBUG_WAIT"));
00219    if (!mSlaveDebug.isEmpty())
00220    {
00221       qWarning("Klauncher running in slave-debug mode for slaves of protocol '%s'", qPrintable(mSlaveDebug));
00222    }
00223    mSlaveValgrind = QString::fromLocal8Bit(qgetenv("KDE_SLAVE_VALGRIND"));
00224    if (!mSlaveValgrind.isEmpty())
00225    {
00226       mSlaveValgrindSkin = QString::fromLocal8Bit(qgetenv("KDE_SLAVE_VALGRIND_SKIN"));
00227       qWarning("Klauncher running slaves through valgrind for slaves of protocol '%s'", qPrintable(mSlaveValgrind));
00228    }
00229 #ifdef Q_WS_WIN
00230    kDebug(7016) << "LAUNCHER_OK";
00231 #else
00232    klauncher_header request_header;
00233    request_header.cmd = LAUNCHER_OK;
00234    request_header.arg_length = 0;
00235    write(kdeinitSocket, &request_header, sizeof(request_header));
00236 #endif
00237 }
00238 
00239 KLauncher::~KLauncher()
00240 {
00241    close();
00242    g_klauncher_self = NULL;
00243 }
00244 
00245 void KLauncher::close()
00246 {
00247 #ifdef Q_WS_X11
00248    if( mCached_dpy != NULL )
00249    {
00250        XCloseDisplay( mCached_dpy );
00251        mCached_dpy = NULL;
00252    }
00253 #endif
00254 }
00255 
00256 void
00257 KLauncher::destruct()
00258 {
00259     if (g_klauncher_self)
00260         g_klauncher_self->close();
00261     // We don't delete the app here, that's intentional.
00262     ::_exit(255);
00263 }
00264 
00265 void KLauncher::setLaunchEnv(const QString &name, const QString &value)
00266 {
00267 #ifdef Q_WS_WIN
00268 
00269 #else
00270    klauncher_header request_header;
00271    QByteArray requestData;
00272    requestData.append(name.toLocal8Bit()).append('\0').append(value.toLocal8Bit()).append('\0');
00273    request_header.cmd = LAUNCHER_SETENV;
00274    request_header.arg_length = requestData.size();
00275    write(kdeinitSocket, &request_header, sizeof(request_header));
00276    write(kdeinitSocket, requestData.data(), request_header.arg_length);
00277 #endif
00278 }
00279 
00280 #ifndef Q_WS_WIN
00281 /*
00282  * Read 'len' bytes from 'sock' into buffer.
00283  * returns -1 on failure, 0 on no data.
00284  */
00285 static int
00286 read_socket(int sock, char *buffer, int len)
00287 {
00288   ssize_t result;
00289   int bytes_left = len;
00290     while (bytes_left > 0) {
00291         // in case we get a request to start an application and data arrive
00292         // to kdeinitSocket at the same time, requestStart() will already
00293         // call slotKDEInitData(), so we must check there's still something
00294         // to read, otherwise this would block
00295 
00296         // Same thing if kdeinit dies without warning.
00297 
00298         fd_set in;
00299         timeval tm = { 30, 0 }; // 30 seconds timeout, so we're not stuck in case kdeinit dies on us
00300         FD_ZERO ( &in );
00301         FD_SET( sock, &in );
00302         select( sock + 1, &in, 0, 0, &tm );
00303         if( !FD_ISSET( sock, &in )) {
00304             kDebug(7016) << "read_socket" << sock << "nothing to read, kdeinit4 must be dead";
00305             return -1;
00306         }
00307 
00308      result = read(sock, buffer, bytes_left);
00309      if (result > 0)
00310      {
00311         buffer += result;
00312         bytes_left -= result;
00313      }
00314      else if (result == 0)
00315         return -1;
00316      else if ((result == -1) && (errno != EINTR))
00317         return -1;
00318   }
00319   return 0;
00320 }
00321 
00322 
00323 void
00324 KLauncher::slotKDEInitData(int)
00325 {
00326    klauncher_header request_header;
00327    QByteArray requestData;
00328 
00329    int result = read_socket(kdeinitSocket, (char *) &request_header,
00330                             sizeof( request_header));
00331    if (result == -1)
00332    {
00333       kDebug(7016) << "Exiting on read_socket errno:" << errno;
00334       KDE_signal( SIGHUP, SIG_IGN);
00335       KDE_signal( SIGTERM, SIG_IGN);
00336       destruct(); // Exit!
00337    }
00338    requestData.resize(request_header.arg_length);
00339    result = read_socket(kdeinitSocket, (char *) requestData.data(),
00340                         request_header.arg_length);
00341 
00342    processRequestReturn(request_header.cmd,requestData);
00343 
00344 }
00345 #endif
00346 
00347 void KLauncher::processRequestReturn(int status, const QByteArray &requestData)
00348 {
00349    if (status == LAUNCHER_CHILD_DIED)
00350    {
00351      long *request_data;
00352      request_data = (long *) requestData.data();
00353      processDied(request_data[0], request_data[1]);
00354      return;
00355    }
00356    if (lastRequest && (status == LAUNCHER_OK))
00357    {
00358      long *request_data;
00359      request_data = (long *) requestData.data();
00360      lastRequest->pid = (pid_t) (*request_data);
00361      kDebug(7016).nospace() << lastRequest->name << " (pid " << lastRequest->pid <<
00362         ") up and running.";
00363      switch(lastRequest->dbus_startup_type)
00364      {
00365        case KService::DBusNone:
00366          lastRequest->status = KLaunchRequest::Running;
00367          break;
00368        case KService::DBusUnique:
00369        case KService::DBusWait:
00370        case KService::DBusMulti:
00371          lastRequest->status = KLaunchRequest::Launching;
00372          break;
00373      }
00374      lastRequest = 0;
00375      return;
00376    }
00377    if (lastRequest && (status == LAUNCHER_ERROR))
00378    {
00379      lastRequest->status = KLaunchRequest::Error;
00380      kDebug(7016) << lastRequest->name << " failed." << endl;
00381      if (!requestData.isEmpty())
00382         lastRequest->errorMsg = QString::fromUtf8((char *) requestData.data());
00383      lastRequest = 0;
00384      return;
00385    }
00386 
00387    kWarning(7016)<< "Unexpected request return" << (unsigned int) status;
00388 }
00389 
00390 void
00391 KLauncher::processDied(pid_t pid, long exitStatus)
00392 {
00393 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00394     kDebug(7016) << pid << "exitStatus=" << exitStatus;
00395 #else
00396     Q_UNUSED(exitStatus);
00397     // We should probably check the exitStatus for the uniqueapp case?
00398 #endif
00399    foreach (KLaunchRequest *request, requestList)
00400    {
00401 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00402        kDebug(7016) << "  had pending request" << request->pid;
00403 #endif
00404       if (request->pid == pid)
00405       {
00406          if (request->dbus_startup_type == KService::DBusWait)
00407              request->status = KLaunchRequest::Done;
00408          else if ((request->dbus_startup_type == KService::DBusUnique)
00409                   && QDBusConnection::sessionBus().interface()->isServiceRegistered(request->dbus_name)) {
00410              request->status = KLaunchRequest::Running;
00411 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00412              kDebug(7016) << pid << "running as a unique app";
00413 #endif
00414          } else {
00415              request->status = KLaunchRequest::Error;
00416 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00417              kDebug(7016) << pid << "died, requestDone. status=" << request->status;
00418 #endif
00419          }
00420          requestDone(request);
00421          return;
00422       }
00423    }
00424 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00425    kDebug(7016) << "found no pending requests for PID" << pid;
00426 #endif
00427 }
00428 
00429 static bool matchesPendingRequest(const QString& appId, const QString& pendingAppId)
00430 {
00431     // appId just registered, e.g. org.koffice.kword-12345
00432     // Let's see if this is what pendingAppId (e.g. org.koffice.kword or *.kword) was waiting for.
00433 
00434     const QString newAppId = appId.left(appId.lastIndexOf(QLatin1Char('-'))); // strip out the -12345 if present.
00435 
00436     //kDebug() << "appId=" << appId << "newAppId=" << newAppId << "pendingAppId=" << pendingAppId;
00437 
00438     if (pendingAppId.startsWith(QLatin1String("*."))) {
00439         const QString pendingName = pendingAppId.mid(2);
00440         const QString appName = newAppId.mid(newAppId.lastIndexOf(QLatin1Char('.'))+1);
00441         //kDebug() << "appName=" << appName;
00442         return appName == pendingName;
00443     }
00444 
00445     return newAppId == pendingAppId;
00446 }
00447 
00448 void
00449 KLauncher::slotNameOwnerChanged(const QString &appId, const QString &oldOwner,
00450                                 const QString &newOwner)
00451 {
00452    Q_UNUSED(oldOwner);
00453    if (appId.isEmpty() || newOwner.isEmpty())
00454       return;
00455 
00456 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00457    kDebug(7016) << "new app" << appId;
00458 #endif
00459    foreach (KLaunchRequest *request, requestList)
00460    {
00461       if (request->status != KLaunchRequest::Launching)
00462          continue;
00463 
00464 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00465       kDebug(7016) << "had pending request" << request->name << s_DBusStartupTypeToString[request->dbus_startup_type] << "dbus_name" << request->dbus_name << request->tolerant_dbus_name;
00466 #endif
00467       // For unique services check the requested service name first
00468       if (request->dbus_startup_type == KService::DBusUnique) {
00469           if ((appId == request->dbus_name) || // just started
00470               QDBusConnection::sessionBus().interface()->isServiceRegistered(request->dbus_name)) { // was already running
00471               request->status = KLaunchRequest::Running;
00472 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00473               kDebug(7016) << "OK, unique app" << request->dbus_name << "is running";
00474 #endif
00475               requestDone(request);
00476               continue;
00477           } else {
00478 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00479               kDebug(7016) << "unique app" << request->dbus_name << "not running yet";
00480 #endif
00481           }
00482       }
00483 
00484       const QString rAppId = !request->tolerant_dbus_name.isEmpty() ? request->tolerant_dbus_name : request->dbus_name;
00485 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00486       //kDebug(7016) << "using" << rAppId << "for matching";
00487 #endif
00488       if (rAppId.isEmpty())
00489           continue;
00490 
00491       if (matchesPendingRequest(appId, rAppId)) {
00492 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00493          kDebug(7016) << "ok, request done";
00494 #endif
00495          request->dbus_name = appId;
00496          request->status = KLaunchRequest::Running;
00497          requestDone(request);
00498          continue;
00499       }
00500    }
00501 }
00502 
00503 void
00504 KLauncher::autoStart(int phase)
00505 {
00506    if( mAutoStart.phase() >= phase )
00507        return;
00508    mAutoStart.setPhase(phase);
00509    if (phase == 0)
00510       mAutoStart.loadAutoStartList();
00511    mAutoTimer.start(0);
00512 }
00513 
00514 void
00515 KLauncher::slotAutoStart()
00516 {
00517    KService::Ptr s;
00518    do
00519    {
00520       QString service = mAutoStart.startService();
00521       if (service.isEmpty())
00522       {
00523          // Done
00524      if( !mAutoStart.phaseDone())
00525      {
00526         mAutoStart.setPhaseDone();
00527             switch( mAutoStart.phase())
00528                 {
00529                 case 0:
00530                     emit autoStart0Done();
00531                     break;
00532                 case 1:
00533                     emit autoStart1Done();
00534                     break;
00535                 case 2:
00536                     emit autoStart2Done();
00537                     break;
00538                 }
00539      }
00540          return;
00541       }
00542       s = new KService(service);
00543    }
00544    while (!start_service(s, QStringList(), QStringList(), "0", false, true, QDBusMessage()));
00545    // Loop till we find a service that we can start.
00546 }
00547 
00548 void
00549 KLauncher::requestDone(KLaunchRequest *request)
00550 {
00551    if ((request->status == KLaunchRequest::Running) ||
00552        (request->status == KLaunchRequest::Done))
00553    {
00554       requestResult.result = 0;
00555       requestResult.dbusName = request->dbus_name;
00556       requestResult.error = QString::fromLatin1(""); // not null, cf assert further down
00557       requestResult.pid = request->pid;
00558    }
00559    else
00560    {
00561       requestResult.result = 1;
00562       requestResult.dbusName = QString();
00563       requestResult.error = i18n("KDEInit could not launch '%1'.", request->name);
00564       if (!request->errorMsg.isEmpty())
00565           requestResult.error += QString::fromLatin1(":\n") + request->errorMsg;
00566       requestResult.pid = 0;
00567 
00568 #ifdef Q_WS_X11
00569       if (!request->startup_dpy.isEmpty())
00570       {
00571          Display* dpy = NULL;
00572          if( (mCached_dpy != NULL) &&
00573               (request->startup_dpy == XDisplayString( mCached_dpy )))
00574             dpy = mCached_dpy;
00575          if( dpy == NULL )
00576             dpy = XOpenDisplay(request->startup_dpy);
00577          if( dpy )
00578          {
00579             KStartupInfoId id;
00580             id.initId(request->startup_id);
00581             KStartupInfo::sendFinishX( dpy, id );
00582             if( mCached_dpy != dpy && mCached_dpy != NULL )
00583                XCloseDisplay( mCached_dpy );
00584             mCached_dpy = dpy;
00585          }
00586       }
00587 #endif
00588    }
00589 
00590    if (request->autoStart)
00591    {
00592       mAutoTimer.start(0);
00593    }
00594 
00595    if (request->transaction.type() != QDBusMessage::InvalidMessage)
00596    {
00597       if ( requestResult.dbusName.isNull() ) // null strings can't be sent
00598           requestResult.dbusName = QString();
00599       Q_ASSERT( !requestResult.error.isNull() );
00600       PIDType<sizeof(pid_t)>::PID_t stream_pid = requestResult.pid;
00601       QDBusConnection::sessionBus().send(request->transaction.createReply(QVariantList() << requestResult.result
00602                                      << requestResult.dbusName
00603                                      << requestResult.error
00604                                      << stream_pid));
00605    }
00606 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00607    kDebug(7016) << "removing done request" << request->name << "PID" << request->pid;
00608 #endif
00609 
00610    requestList.removeAll( request );
00611    delete request;
00612 }
00613 
00614 static void appendLong(QByteArray &ba, long l)
00615 {
00616    const int sz = ba.size();
00617    ba.resize(sz + sizeof(long));
00618    memcpy(ba.data() + sz, &l, sizeof(long));
00619 }
00620 
00621 void
00622 KLauncher::requestStart(KLaunchRequest *request)
00623 {
00624 #ifdef Q_WS_WIN
00625    requestList.append( request );
00626    lastRequest = request;
00627 
00628    KProcess *process  = new KProcess;
00629    process->setOutputChannelMode(KProcess::MergedChannels);
00630    connect(process ,SIGNAL(readyReadStandardOutput()),this, SLOT(slotGotOutput()) );
00631    connect(process ,SIGNAL(finished(int, QProcess::ExitStatus)),this, SLOT(slotFinished(int, QProcess::ExitStatus)) );
00632    request->process = process;
00633 
00634 // process.setEnvironment(envlist);
00635    QStringList args;
00636    foreach (const QString &arg, request->arg_list)
00637       args << arg;
00638 
00639    process->setProgram(request->name,args);
00640    process->start();
00641 
00642    if (!process->waitForStarted())
00643    {
00644        processRequestReturn(LAUNCHER_ERROR,"");
00645    }
00646    else
00647    {
00648        request->pid = process->pid();
00649        QByteArray data((char *)&request->pid, sizeof(int));
00650        processRequestReturn(LAUNCHER_OK,data);
00651    }
00652    return;
00653 
00654 #else
00655    requestList.append( request );
00656    // Send request to kdeinit.
00657    klauncher_header request_header;
00658    QByteArray requestData;
00659    requestData.reserve(1024);
00660 
00661    appendLong(requestData, request->arg_list.count() + 1);
00662    requestData.append(request->name.toLocal8Bit());
00663    requestData.append('\0');
00664    foreach (const QString &arg, request->arg_list)
00665        requestData.append(arg.toLocal8Bit()).append('\0');
00666    appendLong(requestData, request->envs.count());
00667    foreach (const QString &env, request->envs)
00668        requestData.append(env.toLocal8Bit()).append('\0');
00669    appendLong(requestData, 0); // avoid_loops, always false here
00670 #ifdef Q_WS_X11
00671    bool startup_notify = !request->startup_id.isNull() && request->startup_id != "0";
00672    if( startup_notify )
00673        requestData.append(request->startup_id).append('\0');
00674 #endif
00675    if (!request->cwd.isEmpty())
00676        requestData.append(QFile::encodeName(request->cwd)).append('\0');
00677 
00678 #ifdef Q_WS_X11
00679    request_header.cmd = startup_notify ? LAUNCHER_EXT_EXEC : LAUNCHER_EXEC_NEW;
00680 #else
00681    request_header.cmd = LAUNCHER_EXEC_NEW;
00682 #endif
00683    request_header.arg_length = requestData.length();
00684 
00685 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00686    kDebug(7016) << "Asking kdeinit to start" << request->name << request->arg_list
00687                 << "cmd=" << commandToString(request_header.cmd);
00688 #endif
00689 
00690    write(kdeinitSocket, &request_header, sizeof(request_header));
00691    write(kdeinitSocket, requestData.data(), requestData.length());
00692 
00693    // Wait for pid to return.
00694    lastRequest = request;
00695    do {
00696       slotKDEInitData( kdeinitSocket );
00697    }
00698    while (lastRequest != 0);
00699 #endif
00700 }
00701 
00702 void KLauncher::exec_blind(const QString &name, const QStringList &arg_list, const QStringList &envs, const QString &startup_id)
00703 {
00704    KLaunchRequest *request = new KLaunchRequest;
00705    request->autoStart = false;
00706    request->name = name;
00707    request->arg_list =  arg_list;
00708    request->dbus_startup_type = KService::DBusNone;
00709    request->pid = 0;
00710    request->status = KLaunchRequest::Launching;
00711    request->envs = envs;
00712    // Find service, if any - strip path if needed
00713    KService::Ptr service = KService::serviceByDesktopName( name.mid( name.lastIndexOf(QLatin1Char('/')) + 1 ));
00714    if (service)
00715        send_service_startup_info(request, service, startup_id.toLocal8Bit(), QStringList());
00716    else // no .desktop file, no startup info
00717        cancel_service_startup_info( request, startup_id.toLocal8Bit(), envs );
00718 
00719    requestStart(request);
00720    // We don't care about this request any longer....
00721    requestDone(request);
00722 }
00723 
00724 
00725 // KDE5: remove
00726 bool
00727 KLauncher::start_service_by_name(const QString &serviceName, const QStringList &urls,
00728     const QStringList &envs, const QString& startup_id, bool blind, const QDBusMessage &msg)
00729 {
00730    KService::Ptr service;
00731    // Find service
00732    service = KService::serviceByName(serviceName);
00733    if (!service)
00734    {
00735       requestResult.result = ENOENT;
00736       requestResult.error = i18n("Could not find service '%1'.", serviceName);
00737       cancel_service_startup_info( NULL, startup_id.toLocal8Bit(), envs ); // cancel it if any
00738       return false;
00739    }
00740    return start_service(service, urls, envs, startup_id.toLocal8Bit(), blind, false, msg);
00741 }
00742 
00743 bool
00744 KLauncher::start_service_by_desktop_path(const QString &serviceName, const QStringList &urls,
00745     const QStringList &envs, const QString& startup_id, bool blind, const QDBusMessage &msg)
00746 {
00747    KService::Ptr service;
00748    // Find service
00749    if (QFileInfo(serviceName).isAbsolute() && QFile::exists(serviceName))
00750    {
00751       // Full path
00752       service = new KService(serviceName);
00753    }
00754    else
00755    {
00756       service = KService::serviceByDesktopPath(serviceName);
00757       // TODO?
00758       //if (!service)
00759       //    service = KService::serviceByStorageId(serviceName); // This method should be named start_service_by_storage_id ideally...
00760    }
00761    if (!service)
00762    {
00763       requestResult.result = ENOENT;
00764       requestResult.error = i18n("Could not find service '%1'.", serviceName);
00765       cancel_service_startup_info( NULL, startup_id.toLocal8Bit(), envs ); // cancel it if any
00766       return false;
00767    }
00768    return start_service(service, urls, envs, startup_id.toLocal8Bit(), blind, false, msg);
00769 }
00770 
00771 bool
00772 KLauncher::start_service_by_desktop_name(const QString &serviceName, const QStringList &urls,
00773     const QStringList &envs, const QString& startup_id, bool blind, const QDBusMessage &msg)
00774 {
00775    KService::Ptr service = KService::serviceByDesktopName(serviceName);
00776    if (!service)
00777    {
00778       requestResult.result = ENOENT;
00779       requestResult.error = i18n("Could not find service '%1'.", serviceName);
00780       cancel_service_startup_info( NULL, startup_id.toLocal8Bit(), envs ); // cancel it if any
00781       return false;
00782    }
00783    return start_service(service, urls, envs, startup_id.toLocal8Bit(), blind, false, msg);
00784 }
00785 
00786 bool
00787 KLauncher::start_service(KService::Ptr service, const QStringList &_urls,
00788                          const QStringList &envs, const QByteArray &startup_id,
00789                          bool blind, bool autoStart, const QDBusMessage &msg)
00790 {
00791    QStringList urls = _urls;
00792    bool runPermitted = KDesktopFile::isAuthorizedDesktopFile(service->entryPath());
00793 
00794    if (!service->isValid() || !runPermitted)
00795    {
00796       requestResult.result = ENOEXEC;
00797       if (service->isValid())
00798          requestResult.error = i18n("Service '%1' must be executable to run.", service->entryPath());
00799       else
00800          requestResult.error = i18n("Service '%1' is malformatted.", service->entryPath());
00801       cancel_service_startup_info( NULL, startup_id, envs ); // cancel it if any
00802       return false;
00803    }
00804    KLaunchRequest *request = new KLaunchRequest;
00805    request->autoStart = autoStart;
00806 
00807    if ((urls.count() > 1) && !service->allowMultipleFiles())
00808    {
00809       // We need to launch the application N times. That sucks.
00810       // We ignore the result for application 2 to N.
00811       // For the first file we launch the application in the
00812       // usual way. The reported result is based on this
00813       // application.
00814       QStringList::ConstIterator it = urls.constBegin();
00815       for(++it;
00816           it != urls.constEnd();
00817           ++it)
00818       {
00819          QStringList singleUrl;
00820          singleUrl.append(*it);
00821          QByteArray startup_id2 = startup_id;
00822          if( !startup_id2.isEmpty() && startup_id2 != "0" )
00823              startup_id2 = "0"; // can't use the same startup_id several times
00824          start_service( service, singleUrl, envs, startup_id2, true, false, msg);
00825       }
00826       QString firstURL = *(urls.begin());
00827       urls.clear();
00828       urls.append(firstURL);
00829    }
00830    createArgs(request, service, urls);
00831 
00832    // We must have one argument at least!
00833    if (!request->arg_list.count())
00834    {
00835       requestResult.result = ENOEXEC;
00836       requestResult.error = i18n("Service '%1' is malformatted.", service->entryPath());
00837       delete request;
00838       cancel_service_startup_info( NULL, startup_id, envs );
00839       return false;
00840    }
00841 
00842    request->name = request->arg_list.takeFirst();
00843 
00844    if (request->name.endsWith(QLatin1String("/kioexec"))) {
00845        // Special case for kioexec; if createArgs said we were going to use it,
00846        // then we have to expect a kioexec-PID, not a org.kde.finalapp...
00847        // Testcase: konqueror www.kde.org, RMB on link, open with, kruler.
00848 
00849        request->dbus_startup_type = KService::DBusMulti;
00850        request->dbus_name = QString::fromLatin1("org.kde.kioexec");
00851    } else {
00852        request->dbus_startup_type = service->dbusStartupType();
00853 
00854        if ((request->dbus_startup_type == KService::DBusUnique) ||
00855            (request->dbus_startup_type == KService::DBusMulti)) {
00856            const QVariant v = service->property(QLatin1String("X-DBUS-ServiceName"));
00857            if (v.isValid()) {
00858                request->dbus_name = v.toString();
00859            }
00860            if (request->dbus_name.isEmpty()) {
00861                const QString binName = KRun::binaryName(service->exec(), true);
00862                request->dbus_name = QString::fromLatin1("org.kde.") + binName;
00863                request->tolerant_dbus_name = QString::fromLatin1("*.") + binName;
00864            }
00865        }
00866    }
00867 
00868 #ifdef KLAUNCHER_VERBOSE_OUTPUT
00869    kDebug(7016) << "name=" << request->name << "dbus_name=" << request->dbus_name
00870                 << "startup type=" << s_DBusStartupTypeToString[request->dbus_startup_type];
00871 #endif
00872 
00873    request->pid = 0;
00874    request->envs = envs;
00875    send_service_startup_info( request, service, startup_id, envs );
00876 
00877    // Request will be handled later.
00878    if (!blind && !autoStart)
00879    {
00880       msg.setDelayedReply(true);
00881       request->transaction = msg;
00882    }
00883    queueRequest(request);
00884    return true;
00885 }
00886 
00887 void
00888 KLauncher::send_service_startup_info( KLaunchRequest *request, KService::Ptr service, const QByteArray& startup_id,
00889     const QStringList &envs )
00890 {
00891 #ifdef Q_WS_X11
00892     request->startup_id = "0";
00893     if (startup_id == "0")
00894         return;
00895     bool silent;
00896     QByteArray wmclass;
00897     if( !KRun::checkStartupNotify( QString(), service.data(), &silent, &wmclass ))
00898         return;
00899     KStartupInfoId id;
00900     id.initId(startup_id);
00901     QByteArray dpy_str;
00902     foreach (const QString &env, envs) {
00903         if (env.startsWith(QLatin1String("DISPLAY=")))
00904             dpy_str = env.mid(8).toLocal8Bit();
00905     }
00906     Display* dpy = NULL;
00907     if (!dpy_str.isEmpty() && mCached_dpy != NULL && dpy_str != XDisplayString(mCached_dpy))
00908         dpy = mCached_dpy;
00909     if (dpy == NULL)
00910         dpy = XOpenDisplay(dpy_str);
00911     request->startup_id = id.id();
00912     if (dpy == NULL) {
00913         cancel_service_startup_info( request, startup_id, envs );
00914         return;
00915     }
00916 
00917     request->startup_dpy = dpy_str;
00918 
00919     KStartupInfoData data;
00920     data.setName( service->name());
00921     data.setIcon( service->icon());
00922     data.setDescription( i18n( "Launching %1" ,  service->name()));
00923     if( !wmclass.isEmpty())
00924         data.setWMClass( wmclass );
00925     if( silent )
00926         data.setSilent( KStartupInfoData::Yes );
00927     // the rest will be sent by kdeinit
00928     KStartupInfo::sendStartupX( dpy, id, data );
00929     if( mCached_dpy != dpy && mCached_dpy != NULL )
00930         XCloseDisplay( mCached_dpy );
00931     mCached_dpy = dpy;
00932     return;
00933 #else
00934     return;
00935 #endif
00936 }
00937 
00938 void
00939 KLauncher::cancel_service_startup_info( KLaunchRequest* request, const QByteArray& startup_id,
00940     const QStringList &envs )
00941 {
00942 #ifdef Q_WS_X11
00943     if( request != NULL )
00944         request->startup_id = "0";
00945     if( !startup_id.isEmpty() && startup_id != "0" )
00946     {
00947         QString dpy_str;
00948         foreach (const QString &env, envs) {
00949             if (env.startsWith(QLatin1String("DISPLAY=")))
00950                 dpy_str = env.mid(8);
00951         }
00952         Display* dpy = NULL;
00953         if( !dpy_str.isEmpty() && mCached_dpy != NULL
00954             && dpy_str != QLatin1String(XDisplayString( mCached_dpy )) )
00955             dpy = mCached_dpy;
00956         if( dpy == NULL )
00957             dpy = XOpenDisplay( dpy_str.toLatin1().constData() );
00958         if( dpy == NULL )
00959             return;
00960         KStartupInfoId id;
00961         id.initId(startup_id);
00962         KStartupInfo::sendFinishX( dpy, id );
00963         if( mCached_dpy != dpy && mCached_dpy != NULL )
00964            XCloseDisplay( mCached_dpy );
00965         mCached_dpy = dpy;
00966     }
00967 #endif
00968 }
00969 
00970 bool
00971 KLauncher::kdeinit_exec(const QString &app, const QStringList &args,
00972                         const QString& workdir, const QStringList &envs,
00973                         const QString &startup_id, bool wait, const QDBusMessage &msg)
00974 {
00975    KLaunchRequest *request = new KLaunchRequest;
00976    request->autoStart = false;
00977    request->arg_list = args;
00978    request->name = app;
00979    if (wait)
00980       request->dbus_startup_type = KService::DBusWait;
00981    else
00982       request->dbus_startup_type = KService::DBusNone;
00983    request->pid = 0;
00984 #ifdef Q_WS_X11
00985    request->startup_id = startup_id.toLocal8Bit();
00986 #endif
00987    request->envs = envs;
00988    request->cwd = workdir;
00989 #ifdef Q_WS_X11
00990    if (!app.endsWith(QLatin1String("kbuildsycoca4"))) { // avoid stupid loop
00991        // Find service, if any - strip path if needed
00992        const QString desktopName = app.mid(app.lastIndexOf(QLatin1Char('/')) + 1);
00993        KService::Ptr service = KService::serviceByDesktopName(desktopName);
00994        if (service)
00995            send_service_startup_info(request, service,
00996                                      request->startup_id, QStringList());
00997        else // no .desktop file, no startup info
00998            cancel_service_startup_info(request, request->startup_id, envs);
00999    }
01000 #endif
01001    msg.setDelayedReply(true);
01002    request->transaction = msg;
01003    queueRequest(request);
01004    return true;
01005 }
01006 
01007 void
01008 KLauncher::queueRequest(KLaunchRequest *request)
01009 {
01010    requestQueue.append( request );
01011    if (!bProcessingQueue)
01012    {
01013       bProcessingQueue = true;
01014       QTimer::singleShot(0, this, SLOT( slotDequeue() ));
01015    }
01016 }
01017 
01018 void
01019 KLauncher::slotDequeue()
01020 {
01021    do {
01022       KLaunchRequest *request = requestQueue.takeFirst();
01023       // process request
01024       request->status = KLaunchRequest::Launching;
01025       requestStart(request);
01026       if (request->status != KLaunchRequest::Launching)
01027       {
01028          // Request handled.
01029 #ifdef KLAUNCHER_VERBOSE_OUTPUT
01030          kDebug(7016) << "Request handled already";
01031 #endif
01032          requestDone( request );
01033          continue;
01034       }
01035    } while(requestQueue.count());
01036    bProcessingQueue = false;
01037 }
01038 
01039 void
01040 KLauncher::createArgs( KLaunchRequest *request, const KService::Ptr service ,
01041                        const QStringList &urls)
01042 {
01043   const QStringList params = KRun::processDesktopExec(*service, urls);
01044 
01045   for(QStringList::ConstIterator it = params.begin();
01046       it != params.end(); ++it)
01047   {
01048      request->arg_list.append(*it);
01049   }
01050   request->cwd = service->path();
01051 }
01052 
01054 
01055 pid_t
01056 KLauncher::requestHoldSlave(const KUrl &url, const QString &app_socket)
01057 {
01058     IdleSlave *slave = 0;
01059     foreach (IdleSlave *p, mSlaveList)
01060     {
01061        if (p->onHold(url))
01062        {
01063           slave = p;
01064           break;
01065        }
01066     }
01067     if (slave)
01068     {
01069        mSlaveList.removeAll(slave);
01070        slave->connect(app_socket);
01071        return slave->pid();
01072     }
01073     return 0;
01074 }
01075 
01076 
01077 pid_t
01078 KLauncher::requestSlave(const QString &protocol,
01079                         const QString &host,
01080                         const QString &app_socket,
01081                         QString &error)
01082 {
01083     IdleSlave *slave = 0;
01084     foreach (IdleSlave *p, mSlaveList)
01085     {
01086        if (p->match(protocol, host, true))
01087        {
01088           slave = p;
01089           break;
01090        }
01091     }
01092     if (!slave)
01093     {
01094        foreach (IdleSlave *p, mSlaveList)
01095        {
01096           if (p->match(protocol, host, false))
01097           {
01098              slave = p;
01099              break;
01100           }
01101        }
01102     }
01103     if (!slave)
01104     {
01105        foreach (IdleSlave *p, mSlaveList)
01106        {
01107           if (p->match(protocol, QString(), false))
01108           {
01109              slave = p;
01110              break;
01111           }
01112        }
01113     }
01114     if (slave)
01115     {
01116        mSlaveList.removeAll(slave);
01117        slave->connect(app_socket);
01118        return slave->pid();
01119     }
01120 
01121     QString name = KProtocolInfo::exec(protocol);
01122     if (name.isEmpty())
01123     {
01124     error = i18n("Unknown protocol '%1'.\n", protocol);
01125         return 0;
01126     }
01127 
01128     QStringList arg_list;
01129 #ifdef Q_WS_WIN
01130     arg_list << name;
01131     arg_list << protocol;
01132     arg_list << mConnectionServer.address();
01133     arg_list << app_socket;
01134     name = KStandardDirs::findExe(QLatin1String("kioslave"));
01135 #else
01136     QString arg1 = protocol;
01137     QString arg2 = mConnectionServer.address();
01138     QString arg3 = app_socket;
01139     arg_list.append(arg1);
01140     arg_list.append(arg2);
01141     arg_list.append(arg3);
01142 #endif
01143 
01144     kDebug(7016) << "KLauncher: launching new slave " << name << " with protocol=" << protocol
01145      << " args=" << arg_list << endl;
01146 
01147 #ifdef Q_OS_UNIX
01148     if (mSlaveDebug == arg1)
01149     {
01150        klauncher_header request_header;
01151        request_header.cmd = LAUNCHER_DEBUG_WAIT;
01152        request_header.arg_length = 0;
01153        write(kdeinitSocket, &request_header, sizeof(request_header));
01154     }
01155     if (mSlaveValgrind == arg1)
01156     {
01157        arg_list.prepend(KLibLoader::findLibrary(name));
01158        arg_list.prepend(KStandardDirs::locate("exe", QString::fromLatin1("kioslave")));
01159        name = QString::fromLatin1("valgrind");
01160        if (!mSlaveValgrindSkin.isEmpty()) {
01161            arg_list.prepend(QLatin1String("--tool=") + mSlaveValgrindSkin);
01162        } else
01163        arg_list.prepend(QLatin1String("--tool=memcheck"));
01164     }
01165 #endif
01166     KLaunchRequest *request = new KLaunchRequest;
01167     request->autoStart = false;
01168     request->name = name;
01169     request->arg_list =  arg_list;
01170     request->dbus_startup_type = KService::DBusNone;
01171     request->pid = 0;
01172 #ifdef Q_WS_X11
01173     request->startup_id = "0";
01174 #endif
01175     request->status = KLaunchRequest::Launching;
01176     requestStart(request);
01177     pid_t pid = request->pid;
01178 
01179 //    kDebug(7016) << "Slave launched, pid = " << pid;
01180 
01181     // We don't care about this request any longer....
01182     requestDone(request);
01183     if (!pid)
01184     {
01185        error = i18n("Error loading '%1'.\n", name);
01186     }
01187     return pid;
01188 }
01189 
01190 void
01191 KLauncher::waitForSlave(int pid, const QDBusMessage &msg)
01192 {
01193     foreach (IdleSlave *slave, mSlaveList)
01194     {
01195         if (slave->pid() == static_cast<pid_t>(pid))
01196            return; // Already here.
01197     }
01198     SlaveWaitRequest *waitRequest = new SlaveWaitRequest;
01199     msg.setDelayedReply(true);
01200     waitRequest->transaction = msg;
01201     waitRequest->pid = static_cast<pid_t>(pid);
01202     mSlaveWaitRequest.append(waitRequest);
01203 }
01204 
01205 void
01206 KLauncher::acceptSlave()
01207 {
01208     IdleSlave *slave = new IdleSlave(this);
01209     mConnectionServer.setNextPendingConnection(&slave->mConn);
01210     mSlaveList.append(slave);
01211     connect(slave, SIGNAL(destroyed()), this, SLOT(slotSlaveGone()));
01212     connect(slave, SIGNAL(statusUpdate(IdleSlave *)),
01213            this, SLOT(slotSlaveStatus(IdleSlave *)));
01214     if (!mTimer.isActive())
01215     {
01216        mTimer.start(1000*10);
01217     }
01218 }
01219 
01220 void
01221 KLauncher::slotSlaveStatus(IdleSlave *slave)
01222 {
01223     QMutableListIterator<SlaveWaitRequest *> it(mSlaveWaitRequest);
01224     while(it.hasNext())
01225     {
01226        SlaveWaitRequest *waitRequest = it.next();
01227        if (waitRequest->pid == slave->pid())
01228        {
01229            QDBusConnection::sessionBus().send(waitRequest->transaction.createReply());
01230           it.remove();
01231           delete waitRequest;
01232        }
01233     }
01234 }
01235 
01236 void
01237 KLauncher::slotSlaveGone()
01238 {
01239     IdleSlave *slave = (IdleSlave *) sender();
01240     mSlaveList.removeAll(slave);
01241     if ((mSlaveList.count() == 0) && (mTimer.isActive()))
01242     {
01243        mTimer.stop();
01244     }
01245 }
01246 
01247 void
01248 KLauncher::idleTimeout()
01249 {
01250     bool keepOneFileSlave=true;
01251     time_t now = time(0);
01252     foreach (IdleSlave *slave, mSlaveList)
01253     {
01254         if ((slave->protocol()==QLatin1String("file")) && (keepOneFileSlave))
01255            keepOneFileSlave=false;
01256         else if (slave->age(now) > SLAVE_MAX_IDLE)
01257         {
01258            // killing idle slave
01259            delete slave;
01260         }
01261     }
01262 }
01263 
01264 void KLauncher::reparseConfiguration()
01265 {
01266    KProtocolManager::reparseConfiguration();
01267    foreach (IdleSlave *slave, mSlaveList)
01268       slave->reparseConfiguration();
01269 }
01270 
01271 #ifdef Q_WS_WIN
01272 void
01273 KLauncher::slotGotOutput()
01274 {
01275   KProcess *p = static_cast<KProcess *>(sender());
01276   QByteArray _stdout = p->readAllStandardOutput();
01277   kDebug(7016) << _stdout.data();
01278 }
01279 
01280 void
01281 KLauncher::slotFinished(int exitCode, QProcess::ExitStatus exitStatus )
01282 {
01283     KProcess *p = static_cast<KProcess *>(sender());
01284     kDebug(7016) << "process finished exitcode=" << exitCode << "exitStatus=" << exitStatus;
01285 
01286     foreach (KLaunchRequest *request, requestList)
01287     {
01288         if (request->process == p)
01289         {
01290 #ifdef KLAUNCHER_VERBOSE_OUTPUT
01291             kDebug(7016) << "found KProcess, request done";
01292 #endif
01293             if (exitCode == 0  && exitStatus == QProcess::NormalExit)
01294                 request->status = KLaunchRequest::Done;
01295             else
01296                 request->status = KLaunchRequest::Error;
01297             requestDone(request);
01298             request->process = 0;
01299         }
01300     }
01301     delete p;
01302 }
01303 #endif
01304 
01305 void KLauncher::terminate_kdeinit()
01306 {
01307     kDebug(7016);
01308 #ifndef Q_WS_WIN
01309     klauncher_header request_header;
01310     request_header.cmd = LAUNCHER_TERMINATE_KDEINIT;
01311     request_header.arg_length = 0;
01312     write(kdeinitSocket, &request_header, sizeof(request_header));
01313 #endif
01314 }
01315 
01316 #include "klauncher.moc"

KInit

Skip menu "KInit"
  • Main Page
  • Class Hierarchy
  • Alphabetical List
  • Class List
  • File List
  • Class Members
  • Related Pages

kdelibs

Skip menu "kdelibs"
  • DNSSD
  • Interfaces
  •   KHexEdit
  •   KMediaPlayer
  •   KSpeech
  •   KTextEditor
  • Kate
  • kconf_update
  • KDE3Support
  •   KUnitTest
  • KDECore
  • KDED
  • KDEsu
  • KDEUI
  • KDocTools
  • KFile
  • KHTML
  • KImgIO
  • KInit
  • kio
  • KIOSlave
  • KJS
  •   KJS-API
  •   WTF
  • kjsembed
  • KNewStuff
  • KParts
  • KPty
  • Kross
  • KUtils
  • Nepomuk
  • Plasma
  • Solid
  • Sonnet
  • ThreadWeaver
Generated for kdelibs by doxygen 1.6.1
This website is maintained by Adriaan de Groot and Allen Winter.
KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal