Messagelib

editorwatcher.cpp
1 /*
2  SPDX-FileCopyrightText: 2007 Volker Krause <[email protected]>
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "editorwatcher.h"
8 #include <config-messagecomposer.h>
9 
10 #include "messagecomposer_debug.h"
11 
12 #include <KApplicationTrader>
13 #include <KIO/DesktopExecParser>
14 #include <KLocalizedString>
15 #include <KMessageBox>
16 #include <KOpenWithDialog>
17 #include <KProcess>
18 
19 #include <QSocketNotifier>
20 
21 #include <cassert>
22 #include <memory>
23 
24 // inotify stuff taken from kdelibs/kio/kio/kdirwatch.cpp
25 #ifdef HAVE_SYS_INOTIFY_H
26 #include <fcntl.h>
27 #include <sys/inotify.h>
28 #include <sys/ioctl.h>
29 #include <unistd.h>
30 #endif
31 
32 using namespace MessageComposer;
33 
34 EditorWatcher::EditorWatcher(const QUrl &url, const QString &mimeType, OpenWithOption option, QObject *parent, QWidget *parentWidget)
35  : QObject(parent)
36  , mUrl(url)
37  , mMimeType(mimeType)
38  , mParentWidget(parentWidget)
39  , mOpenWithOption(option)
40 {
41  assert(mUrl.isLocalFile());
42  mTimer.setSingleShot(true);
43  connect(&mTimer, &QTimer::timeout, this, &EditorWatcher::checkEditDone);
44 }
45 
46 EditorWatcher::~EditorWatcher()
47 {
48 #ifdef HAVE_SYS_INOTIFY_H
49  ::close(mInotifyFd);
50 #endif
51 }
52 
53 EditorWatcher::ErrorEditorWatcher EditorWatcher::start()
54 {
55  // find an editor
56  QList<QUrl> list;
57  list.append(mUrl);
59  if ((mOpenWithOption == OpenWithDialog) || !offer) {
60  std::unique_ptr<KOpenWithDialog> dlg(new KOpenWithDialog(list, i18n("Edit with:"), QString(), mParentWidget));
61  const int dlgrc = dlg->exec();
62  if (dlgrc) {
63  offer = dlg->service();
64  }
65  if (!dlgrc) {
66  return Canceled;
67  }
68  if (!offer) {
69  return NoServiceFound;
70  }
71  }
72 
73 #ifdef HAVE_SYS_INOTIFY_H
74  // monitor file
75  mInotifyFd = inotify_init();
76  if (mInotifyFd > 0) {
77  (void)fcntl(mInotifyFd, F_SETFD, FD_CLOEXEC);
78  mInotifyWatch = inotify_add_watch(mInotifyFd, mUrl.path().toLatin1().constData(), IN_CLOSE | IN_OPEN | IN_MODIFY | IN_ATTRIB);
79  if (mInotifyWatch >= 0) {
80  auto sn = new QSocketNotifier(mInotifyFd, QSocketNotifier::Read, this);
81  connect(sn, &QSocketNotifier::activated, this, &EditorWatcher::inotifyEvent);
82  mHaveInotify = true;
83  mFileModified = false;
84  }
85  } else {
86  qCWarning(MESSAGECOMPOSER_LOG()) << "Failed to activate INOTIFY!";
87  }
88 #endif
89 
90  // start the editor
91  KIO::DesktopExecParser parser(*offer, list);
92  parser.setUrlsAreTempFiles(false);
93  const QStringList params = parser.resultingArguments();
94  mEditor = new KProcess(this);
95  mEditor->setProgram(params);
96  connect(mEditor, &KProcess::finished, this, &EditorWatcher::editorExited);
97  mEditor->start();
98  if (!mEditor->waitForStarted()) {
99  return CannotStart;
100  }
101  mEditorRunning = true;
102 
103  mEditTime.start();
104  return NoError;
105 }
106 
107 bool EditorWatcher::fileChanged() const
108 {
109  return mFileModified;
110 }
111 
112 QUrl EditorWatcher::url() const
113 {
114  return mUrl;
115 }
116 
117 void EditorWatcher::inotifyEvent()
118 {
119  assert(mHaveInotify);
120 
121 #ifdef HAVE_SYS_INOTIFY_H
122  int pending = -1;
123  int offsetStartRead = 0; // where we read into buffer
124  char buf[8192];
125  assert(mInotifyFd > -1);
126  ioctl(mInotifyFd, FIONREAD, &pending);
127 
128  while (pending > 0) {
129  const int bytesToRead = qMin(pending, (int)sizeof(buf) - offsetStartRead);
130 
131  int bytesAvailable = read(mInotifyFd, &buf[offsetStartRead], bytesToRead);
132  pending -= bytesAvailable;
133  bytesAvailable += offsetStartRead;
134  offsetStartRead = 0;
135 
136  int offsetCurrent = 0;
137  while (bytesAvailable >= (int)sizeof(struct inotify_event)) {
138  const struct inotify_event *const event = (struct inotify_event *)&buf[offsetCurrent];
139  const int eventSize = sizeof(struct inotify_event) + event->len;
140  if (bytesAvailable < eventSize) {
141  break;
142  }
143 
144  bytesAvailable -= eventSize;
145  offsetCurrent += eventSize;
146  if (event->mask & IN_OPEN) {
147  mFileOpen = true;
148  }
149  if (event->mask & IN_CLOSE) {
150  mFileOpen = false;
151  }
152  if (event->mask & (IN_MODIFY | IN_ATTRIB)) {
153  mFileModified = true;
154  }
155  }
156  if (bytesAvailable > 0) {
157  // copy partial event to beginning of buffer
158  memmove(buf, &buf[offsetCurrent], bytesAvailable);
159  offsetStartRead = bytesAvailable;
160  }
161  }
162 #endif
163  mTimer.start(500);
164 }
165 
166 void EditorWatcher::editorExited()
167 {
168  mEditorRunning = false;
169  mTimer.start(500);
170 }
171 
172 void EditorWatcher::checkEditDone()
173 {
174  if (mEditorRunning || (mFileOpen && mHaveInotify) || mDone) {
175  return;
176  }
177 
178  static QStringList readOnlyMimeTypes;
179  if (readOnlyMimeTypes.isEmpty()) {
180  readOnlyMimeTypes << QStringLiteral("message/rfc822") << QStringLiteral("application/pdf");
181  }
182 
183  // protect us against double-deletion by calling this method again while
184  // the subeventloop of the message box is running
185  mDone = true;
186 
187  // check if it's a mime type that's mostly handled read-only
188  const bool isReadOnlyMimeType = (readOnlyMimeTypes.contains(mMimeType) || mMimeType.startsWith(QLatin1String("image/")));
189 
190  // nobody can edit that fast, we seem to be unable to detect
191  // when the editor will be closed
192  if (mEditTime.elapsed() <= 3000 && !isReadOnlyMimeType) {
193  KMessageBox::information(mParentWidget,
194  i18n("KMail is unable to detect when the chosen editor is closed. "
195  "To avoid data loss, editing the attachment will be aborted."),
196  i18nc("@title:window", "Unable to edit attachment"),
197  QStringLiteral("UnableToEditAttachment"));
198  }
199 
200  Q_EMIT editDone(this);
201  deleteLater();
202 }
203 
204 #include "moc_editorwatcher.cpp"
void append(const T &value)
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
Q_EMITQ_EMIT
void setSingleShot(bool singleShot)
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
void finished(int exitCode)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void start()
EditorWatcher(const QUrl &url, const QString &mimeType, OpenWithOption option, QObject *parent, QWidget *parentWidget)
Constructs an EditorWatcher.
void deleteLater()
KSERVICE_EXPORT KService::Ptr preferredService(const QString &mimeType)
void start(int msec)
virtual bool event(QEvent *e)
QString i18n(const char *text, const TYPE &arg...)
void timeout()
bool waitForStarted(int msecs)
bool isEmpty() const const
qint64 elapsed() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString path(QUrl::ComponentFormattingOptions options) const const
const char * constData() const const
bool isLocalFile() const const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QVariant read(const QByteArray &data, int versionOverride=0)
void information(QWidget *parent, const QString &text, const QString &title=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
void activated(QSocketDescriptor socket, QSocketNotifier::Type type)
void setProgram(const QString &exe, const QStringList &args=QStringList())
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Oct 3 2023 03:53:47 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.