Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions src/fileops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
#include <QStorageInfo>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QThread>
#include <QUrl>
#include <QTimer>
#include <QtConcurrent>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <poll.h>
#include <sys/stat.h>
#include <unistd.h>

Expand All @@ -39,6 +41,8 @@
static QList<FileOps::Change> s_undo, s_redo;
static QList<FileOps *> s_everyone;
static constexpr int historyLength = 50;
// How long a copy waits on a pipe with nothing in it before it looks whether it's been asked to stop.
static constexpr int stopCheckMs = 100;

// Work under way in this Folio, whichever pane started it. Panes work side
// by side, but an undo or redo runs alone, so nothing changes the items it
Expand Down Expand Up @@ -223,8 +227,23 @@ static bool writeAll(int fd, const char *data, qint64 size) {
// it's made with them, so a private file is never open to others while it's
// copied.
static bool copyFile(const QString &from, const QString &to, FileOps::Job *job) {
QFile in(from), out(to);
if (!in.open(QIODevice::ReadOnly) || !out.open(QIODevice::WriteOnly | QIODevice::NewOnly, in.permissions()))
QFile in, out(to);
const QByteArray name = QFile::encodeName(from);
int source;
// Opened without waiting: a pipe nobody writes to would hold a plain open for good, out of Stop's reach.
while ((source = ::open(name.constData(), O_RDONLY | O_NONBLOCK | O_CLOEXEC)) < 0 && errno == EWOULDBLOCK) {
// Only a file another program holds a lease on is refused for not waiting: it's asked for again until the lease is given up, with a look for Stop between.
if (job && job->cancelled)
return false;
QThread::msleep(stopCheckMs);
}
if (source < 0)
return false;
if (!in.open(source, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
::close(source);
return false;
}
if (!out.open(QIODevice::WriteOnly | QIODevice::NewOnly, in.permissions()))
return false;
bool kernel = true, ok = true;
QByteArray buffer;
Expand All @@ -245,12 +264,16 @@ static bool copyFile(const QString &from, const QString &to, FileOps::Job *job)
continue;
}
} else {
// A read of a silent pipe comes back at once with nothing, so the wait is here; a file always has its next piece, and a pipe its end once its writers have gone.
pollfd waiting{in.handle(), POLLIN, 0};
if (::poll(&waiting, 1, stopCheckMs) == 0)
continue;
buffer.resize(1 << 20);
n = ::read(in.handle(), buffer.data(), buffer.size());
if (n > 0 && !writeAll(out.handle(), buffer.constData(), n))
n = -1;
}
if (n < 0 && errno == EINTR)
if (n < 0 && (errno == EINTR || errno == EAGAIN))
continue;
if (n <= 0) {
ok = n == 0;
Expand Down
103 changes: 103 additions & 0 deletions tests/folio_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
});
}

// Longer than a few of a copy's looks for Stop, which come 100 ms apart.
static constexpr int severalStopChecksMs = 300;

// A temporary folder on another drive than QTemporaryDir's, for what has to
// cross drives: beside the tests, when they're on another drive. Null when
// they aren't.
Expand Down Expand Up @@ -1847,6 +1850,106 @@ private slots:
QVERIFY(ops.busy().isEmpty());
}

void fileOpsCancelStopsACopyWaitingOnAPipe()
{
// Stop ends a copy of a pipe nobody writes to: with no writer at all, then with one that holds the pipe open and says nothing.
QTemporaryDir from, to;
const QByteArray pipe = QFile::encodeName(from.filePath("pipe"));
QVERIFY(mkfifo(pipe.constData(), 0600) == 0);
for (const bool silentWriter : {false, true}) {
const int holder = silentWriter ? open(pipe.constData(), O_RDWR | O_NONBLOCK) : -1;
QCOMPARE(holder >= 0, silentWriter);
FileOps::Job job;
QString error;
QFuture<bool> copied = QtConcurrent::run(
[&] { return FileOps::copyRecursively(from.filePath("pipe"), to.filePath("pipe"), &error, false, &job); });
// A copy Stop didn't reach ends once the pipe is let go, so a run that fails ends too.
const auto finish = qScopeGuard([&] {
const int writer = open(pipe.constData(), O_WRONLY | O_NONBLOCK);
if (writer >= 0)
close(writer);
if (holder >= 0)
close(holder);
copied.waitForFinished();
});
// It waits for a writer, rather than taking nothing yet for the pipe's end.
QTest::qWait(severalStopChecksMs);
QVERIFY(!copied.isFinished());
job.cancelled = true;
QTRY_VERIFY(copied.isFinished());
QVERIFY(!copied.result());
QVERIFY2(error.isEmpty(), qPrintable(error));
QVERIFY(!QFileInfo::exists(to.filePath("pipe")));
}
}

void fileOpsCopyLosesNothingWhenAPipesWriterPauses()
{
// A writer that stops for longer than a look for Stop takes is waited for, and what it sends next still arrives.
QTemporaryDir from, to;
const QByteArray pipe = QFile::encodeName(from.filePath("pipe"));
QVERIFY(mkfifo(pipe.constData(), 0600) == 0);
FileOps::Job job;
QString error;
QFuture<bool> copied = QtConcurrent::run(
[&] { return FileOps::copyRecursively(from.filePath("pipe"), to.filePath("pipe"), &error, false, &job); });
int writer = -1;
const auto finish = qScopeGuard([&] {
if (writer >= 0)
close(writer);
copied.waitForFinished();
});
QVERIFY(QTest::qWaitFor([&] { return (writer = open(pipe.constData(), O_WRONLY | O_NONBLOCK)) >= 0; }));
QCOMPARE(::write(writer, "first ", 6), 6);
QTest::qWait(severalStopChecksMs);
QVERIFY(!copied.isFinished());
QCOMPARE(::write(writer, "second", 6), 6);
close(std::exchange(writer, -1));
QVERIFY(copied.result());
QVERIFY2(error.isEmpty(), qPrintable(error));
QCOMPARE(contents(to.filePath("pipe")), QByteArray("first second"));
}

void fileOpsCopyWaitsForAFileUnderALease()
{
// A file another program holds a write lease on refuses an open that won't wait: the copy waits until the lease is given up, or until Stop.
QTemporaryDir from, to;
write(from.filePath("leased.txt"), "kept");
// Breaking a lease tells its holder with SIGIO, which would end the test.
const auto before = signal(SIGIO, SIG_IGN);
const auto restored = qScopeGuard([before] { signal(SIGIO, before); });
for (const bool stopped : {false, true}) {
const int holder = open(QFile::encodeName(from.filePath("leased.txt")).constData(), O_RDWR);
QVERIFY(holder >= 0);
const bool leased = fcntl(holder, F_SETLEASE, F_WRLCK) == 0;
FileOps::Job job;
QString error;
QFuture<bool> copied = QtConcurrent::run(
[&] { return FileOps::copyRecursively(from.filePath("leased.txt"), to.filePath("leased.txt"), &error, false, &job); });
// Closing the holder gives the lease up, so a run that fails ends too.
const auto finish = qScopeGuard([&] {
close(holder);
copied.waitForFinished();
});
if (!leased)
QSKIP("This filesystem gives no leases.");
QTest::qWait(severalStopChecksMs);
QVERIFY(!copied.isFinished());
if (stopped)
job.cancelled = true;
else
QCOMPARE(fcntl(holder, F_SETLEASE, F_UNLCK), 0);
QTRY_VERIFY(copied.isFinished());
QCOMPARE(copied.result(), !stopped);
QVERIFY2(error.isEmpty(), qPrintable(error));
QCOMPARE(QFileInfo::exists(to.filePath("leased.txt")), !stopped);
if (!stopped) {
QCOMPARE(contents(to.filePath("leased.txt")), QByteArray("kept"));
QVERIFY(QFile::remove(to.filePath("leased.txt")));
}
}
}

void fileOpsCancelledMoveAcrossDrivesKeepsOriginals()
{
// Across drives each original goes once it's copied, so a move
Expand Down