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 ¤tService : 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 }