File indexing completed on 2024-04-28 05:49:20

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2001-2022 Christoph Cullmann <cullmann@kde.org>
0003  * SPDX-FileCopyrightText: 2001-2002 Joseph Wenninger <jowenn@kde.org>
0004  * SPDX-FileCopyrightText: 2001 Anders Lund <anders.lund@lund.tdcadsl.dk>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 #include "kateapp.h"
0010 #include "katerunninginstanceinfo.h"
0011 #include "katewaiter.h"
0012 
0013 #include <KAboutData>
0014 #include <KDBusService>
0015 #include <KLocalizedString>
0016 
0017 // X11 startup handling
0018 #if __has_include(<KStartupInfo>)
0019 #include <KStartupInfo>
0020 #endif
0021 
0022 #include <KWindowSystem>
0023 #include <algorithm>
0024 
0025 #include <QApplication>
0026 #include <QCommandLineParser>
0027 #include <QDBusMessage>
0028 #include <QDBusReply>
0029 #include <QIcon>
0030 #include <QJsonDocument>
0031 #include <QRegularExpression>
0032 #include <QSessionManager>
0033 #include <QStandardPaths>
0034 #include <QStringDecoder>
0035 #include <QVariant>
0036 
0037 #include <qglobal.h>
0038 #include <urlinfo.h>
0039 
0040 #include "SingleApplication/SingleApplication"
0041 
0042 #include "config-kate.h"
0043 
0044 #if HAVE_X11
0045 
0046 #include <KX11Extras>
0047 
0048 #include <private/qtx11extras_p.h>
0049 #endif
0050 
0051 int main(int argc, char **argv)
0052 {
0053     /**
0054      * fork into the background if we don't need to be blocking
0055      * we need to do that early
0056      */
0057     bool detach = true;
0058     for (int i = 1; i < argc; ++i) {
0059         if (!strcmp(argv[i], "-b") || !strcmp(argv[i], "--block") || !strcmp(argv[i], "-i") || !strcmp(argv[i], "--stdin") || !(strcmp(argv[i], "-v"))
0060             || !(strcmp(argv[i], "--version")) || !(strcmp(argv[i], "-h")) || !(strcmp(argv[i], "--help"))) {
0061             detach = false;
0062             break;
0063         }
0064     }
0065 
0066     /**
0067      * Do all needed pre-application init steps, shared between Kate and KWrite
0068      */
0069     KateApp::initPreApplicationCreation(detach);
0070 
0071     /**
0072      * Create application first
0073      * We always use a single application that allows to start multiple instances.
0074      * This allows for communication even without DBus and better testing of these code paths.
0075      */
0076     SingleApplication app(argc, argv, true);
0077     app.setApplicationName(QStringLiteral("kate"));
0078 
0079     /**
0080      * Connect application with translation catalogs, Kate & KWrite share the same one
0081      */
0082     KLocalizedString::setApplicationDomain(QByteArrayLiteral("kate"));
0083 
0084     /**
0085      * construct about data for Kate
0086      */
0087     KAboutData aboutData(QStringLiteral("kate"),
0088                          i18n("Kate"),
0089                          QStringLiteral(KATE_VERSION),
0090                          i18n("Kate - Advanced Text Editor"),
0091                          KAboutLicense::LGPL_V2,
0092                          i18n("(c) 2000-2022 The Kate Authors"),
0093                          // use the other text field to get our mascot into the about dialog
0094                          QStringLiteral("<img height=\"362\" width=\"512\" src=\":/kate/mascot.png\"/>"),
0095                          QStringLiteral("https://kate-editor.org"));
0096 
0097     /**
0098      * right dbus prefix == org.kde.
0099      */
0100     aboutData.setOrganizationDomain("kde.org");
0101 
0102     /**
0103      * desktop file association to make application icon work (e.g. in Wayland window decoration)
0104      */
0105     aboutData.setDesktopFileName(QStringLiteral("org.kde.kate"));
0106 
0107     /**
0108      * authors & co.
0109      * add yourself there, if you helped to work on Kate or KWrite
0110      */
0111     KateApp::fillAuthorsAndCredits(aboutData);
0112 
0113     /**
0114      * set proper Kate icon for our about dialog
0115      */
0116     aboutData.setProgramLogo(QIcon(QStringLiteral(":/kate/kate.svg")));
0117 
0118     /**
0119      * set and register app about data
0120      */
0121     KAboutData::setApplicationData(aboutData);
0122 
0123     /**
0124      * set the program icon
0125      */
0126 #ifndef Q_OS_MACOS // skip this on macOS to have proper mime-type icon visible
0127     QApplication::setWindowIcon(QIcon(QStringLiteral(":/kate/kate.svg")));
0128 #endif
0129 
0130     /**
0131      * Create command line parser and feed it with known options
0132      */
0133     QCommandLineParser parser;
0134     aboutData.setupCommandLine(&parser);
0135 
0136     // -s/--start session option
0137     const QCommandLineOption startSessionOption(QStringList() << QStringLiteral("s") << QStringLiteral("start"),
0138                                                 i18n("Start Kate with a given session."),
0139                                                 i18n("session"));
0140     parser.addOption(startSessionOption);
0141 
0142     // --startanon session option
0143     const QCommandLineOption startAnonymousSessionOption(QStringList() << QStringLiteral("startanon"),
0144                                                          i18n("Start Kate with a new anonymous session, implies '-n'."));
0145     parser.addOption(startAnonymousSessionOption);
0146 
0147     // -n/--new option
0148     const QCommandLineOption startNewInstanceOption(QStringList() << QStringLiteral("n") << QStringLiteral("new"),
0149                                                     i18n("Force start of a new kate instance (is ignored if start is used and another kate instance already "
0150                                                          "has the given session opened), forced if no parameters and no URLs are given at all."));
0151     parser.addOption(startNewInstanceOption);
0152 
0153     // -b/--block option
0154     const QCommandLineOption startBlockingOption(QStringList() << QStringLiteral("b") << QStringLiteral("block"),
0155                                                  i18n("If using an already running kate instance, block until it exits, if URLs given to open."));
0156     parser.addOption(startBlockingOption);
0157 
0158     // -p/--pid option
0159     const QCommandLineOption usePidOption(
0160         QStringList() << QStringLiteral("p") << QStringLiteral("pid"),
0161         i18n("Only try to reuse kate instance with this pid (is ignored if start is used and another kate instance already has the given session opened)."),
0162         i18n("pid"));
0163     parser.addOption(usePidOption);
0164 
0165     // -e/--encoding option
0166     const QCommandLineOption useEncodingOption(QStringList() << QStringLiteral("e") << QStringLiteral("encoding"),
0167                                                i18n("Set encoding for the file to open."),
0168                                                i18n("encoding"));
0169     parser.addOption(useEncodingOption);
0170 
0171     // -l/--line option
0172     const QCommandLineOption gotoLineOption(QStringList() << QStringLiteral("l") << QStringLiteral("line"), i18n("Navigate to this line."), i18n("line"));
0173     parser.addOption(gotoLineOption);
0174 
0175     // -c/--column option
0176     const QCommandLineOption gotoColumnOption(QStringList() << QStringLiteral("c") << QStringLiteral("column"),
0177                                               i18n("Navigate to this column."),
0178                                               i18n("column"));
0179     parser.addOption(gotoColumnOption);
0180 
0181     // -i/--stdin option
0182     const QCommandLineOption readStdInOption(QStringList() << QStringLiteral("i") << QStringLiteral("stdin"), i18n("Read the contents of stdin."));
0183     parser.addOption(readStdInOption);
0184 
0185     // --tempfile option
0186     const QCommandLineOption tempfileOption(QStringList() << QStringLiteral("tempfile"),
0187                                             i18n("The files/URLs opened by the application will be deleted after use"));
0188     parser.addOption(tempfileOption);
0189 
0190     // urls to open
0191     parser.addPositionalArgument(QStringLiteral("urls"), i18n("Documents to open."), i18n("[urls...]"));
0192 
0193     /**
0194      * do the command line parsing
0195      */
0196     parser.process(app);
0197 
0198     /**
0199      * handle standard options
0200      */
0201     aboutData.processCommandLine(&parser);
0202 
0203     /**
0204      * remember the urls we shall open
0205      */
0206     const QStringList urls = parser.positionalArguments();
0207 
0208     /**
0209      * compute if we shall start a new instance or reuse
0210      * an old one
0211      * this will later be updated once more after detecting some
0212      * things about already running kate's, like their sessions
0213      */
0214     bool force_new = parser.isSet(startNewInstanceOption);
0215     if (!force_new) {
0216         if (!(parser.isSet(startSessionOption) || parser.isSet(startNewInstanceOption) || parser.isSet(usePidOption) || parser.isSet(useEncodingOption)
0217               || parser.isSet(gotoLineOption) || parser.isSet(gotoColumnOption) || parser.isSet(readStdInOption))
0218             && (urls.isEmpty())) {
0219             force_new = true;
0220         } else {
0221             force_new = std::any_of(urls.begin(), urls.end(), [](const QString &url) {
0222                 return QFileInfo(url).isDir();
0223             });
0224         }
0225     }
0226 
0227     /**
0228      * only block, if files to open there....
0229      */
0230     const bool needToBlock = parser.isSet(startBlockingOption) && !urls.isEmpty();
0231 
0232     /**
0233      * use dbus, if available for linux and co.
0234      * allows for reuse of running Kate instances
0235      * we have some env var to forbid this for easier testing of the single application code paths: KATE_SKIP_DBUS
0236      */
0237     if (QDBusConnectionInterface *const sessionBusInterface = QDBusConnection::sessionBus().interface();
0238         sessionBusInterface && qEnvironmentVariableIsEmpty("KATE_SKIP_DBUS")) {
0239         /**
0240          * try to get the current running kate instances
0241          */
0242         const auto kateServices = fillinRunningKateAppInstances();
0243 
0244         QString serviceName;
0245         QString start_session;
0246         bool session_already_opened = false;
0247 
0248         // check if we try to start an already opened session
0249         if (parser.isSet(startAnonymousSessionOption)) {
0250             force_new = true;
0251         } else if (parser.isSet(startSessionOption)) {
0252             start_session = parser.value(startSessionOption);
0253             const auto it = std::find_if(kateServices.cbegin(), kateServices.cend(), [&start_session](const auto &instance) {
0254                 return instance.sessionName == start_session;
0255             });
0256             if (it != kateServices.end()) {
0257                 serviceName = it->serviceName;
0258                 force_new = false;
0259                 session_already_opened = true;
0260             }
0261         }
0262 
0263         // if no new instance is forced and no already opened session is requested,
0264         // check if a pid is given, which should be reused.
0265         // two possibilities: pid given or not...
0266         if ((!force_new) && serviceName.isEmpty()) {
0267             if ((parser.isSet(usePidOption)) || (!qEnvironmentVariableIsEmpty("KATE_PID"))) {
0268                 QString usePid = (parser.isSet(usePidOption)) ? parser.value(usePidOption) : QString::fromLocal8Bit(qgetenv("KATE_PID"));
0269 
0270                 serviceName = QLatin1String("org.kde.kate-") + usePid;
0271                 const auto it = std::find_if(kateServices.cbegin(), kateServices.cend(), [&serviceName](const auto &instance) {
0272                     return instance.serviceName == serviceName;
0273                 });
0274                 if (it == kateServices.end()) {
0275                     serviceName.clear();
0276                 }
0277             }
0278         }
0279 
0280         // prefer the Kate instance that got activated last
0281         bool foundRunningService = false;
0282         if (!force_new && serviceName.isEmpty()) {
0283             qint64 lastUsedChosen = 0;
0284             for (const auto &currentService : kateServices) {
0285                 if (currentService.lastActivationChange > lastUsedChosen) {
0286                     serviceName = currentService.serviceName;
0287                     lastUsedChosen = currentService.lastActivationChange;
0288                 }
0289             }
0290         }
0291 
0292         // check again if service is still running
0293         foundRunningService = false;
0294         if (!serviceName.isEmpty()) {
0295             QDBusReply<bool> there = sessionBusInterface->isServiceRegistered(serviceName);
0296             foundRunningService = there.isValid() && there.value();
0297         }
0298 
0299         if (foundRunningService) {
0300             // open given session
0301             if (parser.isSet(startSessionOption) && (!session_already_opened)) {
0302                 QDBusMessage m = QDBusMessage::createMethodCall(serviceName,
0303                                                                 QStringLiteral("/MainApplication"),
0304                                                                 QStringLiteral("org.kde.Kate.Application"),
0305                                                                 QStringLiteral("activateSession"));
0306 
0307                 QVariantList dbusargs;
0308                 dbusargs.append(parser.value(startSessionOption));
0309                 m.setArguments(dbusargs);
0310 
0311                 QDBusConnection::sessionBus().call(m);
0312             }
0313 
0314             QString enc = parser.isSet(useEncodingOption) ? parser.value(useEncodingOption) : QString();
0315 
0316             bool tempfileSet = parser.isSet(tempfileOption);
0317 
0318             QStringList tokens;
0319 
0320             // open given files...
0321             for (int i = 0; i < urls.size(); ++i) {
0322                 const QString &url = urls[i];
0323                 QDBusMessage m = QDBusMessage::createMethodCall(serviceName,
0324                                                                 QStringLiteral("/MainApplication"),
0325                                                                 QStringLiteral("org.kde.Kate.Application"),
0326                                                                 QStringLiteral("tokenOpenUrlAt"));
0327 
0328                 UrlInfo info(url);
0329                 QVariantList dbusargs;
0330 
0331                 // convert to an url
0332                 dbusargs.append(info.url.toString());
0333                 dbusargs.append(info.cursor.line());
0334                 dbusargs.append(info.cursor.column());
0335                 dbusargs.append(enc);
0336                 dbusargs.append(tempfileSet);
0337                 m.setArguments(dbusargs);
0338 
0339                 QDBusMessage res = QDBusConnection::sessionBus().call(m);
0340                 if (res.type() == QDBusMessage::ReplyMessage) {
0341                     if (res.arguments().count() == 1) {
0342                         QVariant v = res.arguments()[0];
0343                         if (v.isValid()) {
0344                             QString s = v.toString();
0345                             if ((!s.isEmpty()) && (s != QLatin1String("ERROR"))) {
0346                                 tokens << s;
0347                             }
0348                         }
0349                     }
0350                 }
0351             }
0352 
0353             if (parser.isSet(readStdInOption)) {
0354                 // set chosen codec
0355                 const QString codec_name = parser.isSet(QStringLiteral("encoding")) ? parser.value(QStringLiteral("encoding")) : QString();
0356 
0357                 QFile input;
0358                 input.open(stdin, QIODevice::ReadOnly);
0359                 auto decoder = QStringDecoder(codec_name.toUtf8().constData());
0360                 QString text = decoder.isValid() ? decoder.decode(input.readAll()) : QString::fromLocal8Bit(input.readAll());
0361 
0362                 // normalize line endings, to e.g. catch issues with \r\n on Windows
0363                 text.replace(QRegularExpression(QStringLiteral("\r\n?")), QStringLiteral("\n"));
0364 
0365                 QDBusMessage m = QDBusMessage::createMethodCall(serviceName,
0366                                                                 QStringLiteral("/MainApplication"),
0367                                                                 QStringLiteral("org.kde.Kate.Application"),
0368                                                                 QStringLiteral("openInput"));
0369 
0370                 QVariantList dbusargs;
0371                 dbusargs.append(text);
0372                 dbusargs.append(codec_name);
0373                 m.setArguments(dbusargs);
0374 
0375                 QDBusConnection::sessionBus().call(m);
0376             }
0377 
0378             int line = 0;
0379             int column = 0;
0380             bool nav = false;
0381 
0382             if (parser.isSet(gotoLineOption)) {
0383                 line = parser.value(gotoLineOption).toInt() - 1;
0384                 nav = true;
0385             }
0386 
0387             if (parser.isSet(gotoColumnOption)) {
0388                 column = parser.value(gotoColumnOption).toInt() - 1;
0389                 nav = true;
0390             }
0391 
0392             if (nav) {
0393                 QDBusMessage m = QDBusMessage::createMethodCall(serviceName,
0394                                                                 QStringLiteral("/MainApplication"),
0395                                                                 QStringLiteral("org.kde.Kate.Application"),
0396                                                                 QStringLiteral("setCursor"));
0397 
0398                 QVariantList args;
0399                 args.append(line);
0400                 args.append(column);
0401                 m.setArguments(args);
0402 
0403                 QDBusConnection::sessionBus().call(m);
0404             }
0405 
0406             // activate the used instance
0407             QDBusMessage activateMsg = QDBusMessage::createMethodCall(serviceName,
0408                                                                       QStringLiteral("/MainApplication"),
0409                                                                       QStringLiteral("org.kde.Kate.Application"),
0410                                                                       QStringLiteral("activate"));
0411             if (KWindowSystem::isPlatformWayland()) {
0412                 activateMsg.setArguments({qEnvironmentVariable("XDG_ACTIVATION_TOKEN")});
0413             } else if (KWindowSystem::isPlatformX11()) {
0414 #if HAVE_X11
0415                 activateMsg.setArguments({QString::fromUtf8(QX11Info::nextStartupId())});
0416 #endif
0417             }
0418             QDBusConnection::sessionBus().call(activateMsg);
0419 
0420             // connect dbus signal
0421             if (needToBlock) {
0422                 KateWaiter *waiter = new KateWaiter(serviceName, tokens);
0423                 QDBusConnection::sessionBus().connect(serviceName,
0424                                                       QStringLiteral("/MainApplication"),
0425                                                       QStringLiteral("org.kde.Kate.Application"),
0426                                                       QStringLiteral("exiting"),
0427                                                       waiter,
0428                                                       SLOT(exiting()));
0429                 QDBusConnection::sessionBus().connect(serviceName,
0430                                                       QStringLiteral("/MainApplication"),
0431                                                       QStringLiteral("org.kde.Kate.Application"),
0432                                                       QStringLiteral("documentClosed"),
0433                                                       waiter,
0434                                                       SLOT(documentClosed(QString)));
0435             }
0436 
0437             // KToolInvocation (and KRun) will wait until we register on dbus
0438             KDBusService dbusService(KDBusService::Multiple);
0439             dbusService.unregister();
0440 
0441 #if __has_include(<KStartupInfo>)
0442             // make the world happy, we are started, kind of...
0443             KStartupInfo::appStarted();
0444 #endif
0445 
0446             // We don't want the session manager to restart us on next login
0447             // if we block
0448             if (needToBlock) {
0449                 QObject::connect(
0450                     qApp,
0451                     &QGuiApplication::saveStateRequest,
0452                     qApp,
0453                     [](QSessionManager &session) {
0454                         session.setRestartHint(QSessionManager::RestartNever);
0455                     },
0456                     Qt::DirectConnection);
0457             }
0458 
0459             // this will wait until exiting is emitted by the used instance, if wanted...
0460             return needToBlock ? app.exec() : 0;
0461         }
0462     }
0463 
0464     /**
0465      * if we had no DBus session bus, we can try to use the SingleApplication communication.
0466      * only try to reuse existing kate instances if not already forbidden by arguments
0467      */
0468     else if (!force_new && app.isSecondary()) {
0469         /**
0470          * construct one big message with all urls to open
0471          * later we will add additional data to this
0472          */
0473         QVariantMap message;
0474         QVariantList messageUrls;
0475         for (const QString &url : urls) {
0476             /**
0477              * get url info and pack them into the message as extra element in urls list
0478              */
0479             UrlInfo info(url);
0480             QVariantMap urlMessagePart;
0481             urlMessagePart[QLatin1String("url")] = info.url;
0482             urlMessagePart[QLatin1String("line")] = info.cursor.line();
0483             urlMessagePart[QLatin1String("column")] = info.cursor.column();
0484             messageUrls.append(urlMessagePart);
0485         }
0486         message[QLatin1String("urls")] = messageUrls;
0487 
0488         /**
0489          * try to send message, return success
0490          */
0491         return !app.sendMessage(QJsonDocument::fromVariant(QVariant(message)).toJson(),
0492                                 1000,
0493                                 needToBlock ? SingleApplication::BlockUntilPrimaryExit : SingleApplication::NonBlocking);
0494     }
0495 
0496     /**
0497      * if we arrive here, we need to start a new kate instance!
0498      */
0499 
0500     /**
0501      * construct the real kate app object ;)
0502      * behaves like a singleton, one unique instance
0503      * we are passing our local command line parser to it
0504      */
0505     KateApp kateApp(parser, KateApp::ApplicationKate, QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kate/sessions"));
0506 
0507     /**
0508      * init kate
0509      * if this returns false, we shall exit
0510      * else we may enter the main event loop
0511      */
0512     if (!kateApp.init()) {
0513         return 0;
0514     }
0515 
0516     /**
0517      * finally register this kate instance for dbus, don't die if no dbus is around!
0518      */
0519     const KDBusService dbusService(KDBusService::Multiple | KDBusService::NoExitOnFailure);
0520 
0521     /**
0522      * listen to single application messages in any case
0523      */
0524     QObject::connect(&app, &SingleApplication::receivedMessage, &kateApp, &KateApp::remoteMessageReceived);
0525 
0526     /**
0527      * start main event loop for our application
0528      */
0529     return app.exec();
0530 }