0

I have written a function to check whether a file on disk is already in use. This is to avoid trying to execute a freshly downloaded installer while the antivirus is checking it, which fails.

The (generic) function looks like that:

bool isFileInUse(const QString& filePath)
{
    QFile f(filePath);
    if (!f.exists())
    {
        qDebug() << "File does not exist:" << filePath;
        return false;
    }
    if (!f.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly))
    {
        qDebug() << "File in use:" << filePath;
        return true;
    }
    else
    {
        f.close();
        qDebug() << "File free:" << filePath;
        return false;
    }
}

This works, I have tested manually with an installer (.exe) and it returns the expected result.

But now I want to write a unit test to check that function. I have tried to create a file, and open it with QFile::open(QIODevice::WriteOnly), then call isFileInUse(..) on it, expecting to be already "in use", but it always returns false, i.e. Qt seem to have no problem to open twice the same file even in WriteOnly !

TEST(FilesUtils, isFileInUse)
{
    QTemporaryDir dir;
    const QString filePath = dir.filePath("test.txt");
    createFile(filePath); // open, write dummy data and close the file
    EXPECT_FALSE(FilesUtils::isFileInUse(filePath));

    QFile f(filePath);
    EXPECT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Append)); // OK
    EXPECT_TRUE(FilesUtils::isFileInUse(filePath)); // FAILS, returns false
}

I have tried to open the file with a software like notepad.exe, and it also returns false. Then I tried with Microsoft Word, and there it returns finally true (= file in use). But this is not portable and I cant expect a user to have Word installed on Windows, obviously.

Is there any way to open a file with Qt such that another QFile::open() returns false ? (i.e. lock the file) Or does anyone sees something wrong in the code above ?

drescherjm
  • 10,365
  • 5
  • 44
  • 64
Alexandre
  • 498
  • 2
  • 8
  • 1
    Some editors open the file read the entire contents and close. On Microsoft Word you edit a temporary file which is a copy of the original. When you save the old file gets deleted and the temporary file is renamed to the original name. – drescherjm Oct 08 '20 at 14:40

2 Answers2

1

Once your target file has been created and opened, you should use the open() method with the QIODevice::NewOnly flag if it is to be called again.

 QIODevice::NewOnly | 0x0040    | Fail if the file to be opened already exists. Create  and open the file only if it does not exist. There is a guarantee from the operating system that you are the only one creating and opening the file. Note that this mode implies WriteOnly, and combining it with ReadWrite is allowed. This flag currently only affects QFile. Other classes might use this flag in the future, but until then using this flag with any classes other than QFile may result in undefined behavior. (since Qt 5.11)   

Alternatively you could use QFile::isOpen() to test for prior file opening in function IsFileInUse:

if (f.isOpen()) return true;

Below is the code that proves the point (adapted from OP, which does not run out of the box):

#include <QString>
#include <QFile>
#include <QTemporaryDir>
#include <QtDebug>
#include <iostream>

void createFile(const QString& filePath)
{
    // open, write dummy data and close the file

    QFile f(filePath);
    f.open(QIODevice::WriteOnly | QIODevice::Append);
    f.write("dummy");
    f.close();
}

#define EXPECT_FALSE(X) std::cerr <<  (X == false ? "OK, FALSE" : "NOT FALSE!") << std::endl;
#define EXPECT_TRUE(X)  std::cerr <<  (X == true  ? "OK, TRUE"  : "NOT TRUE!")  << std::endl;

class FilesUtils {
public:
static bool isFileInUse(const QString& filePath)
{
    QFile f(filePath);
    if (!f.exists())
    {
        qDebug() << "File does not exist:" << filePath;
        return false;
    }
    if (!f.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly))
    {
        qDebug() << "File in use:" << filePath;
        return true;
    }
    else
    {
        f.close();
        qDebug() << "File free:" << filePath;
        return false;
    }
}
};


int main()
{
    QTemporaryDir dir;
    const QString filePath = dir.filePath("test.txt");
    if (QFileInfo(filePath).exists()) QFile(filePath).remove();
    createFile(filePath); // open, write dummy data and close the file
    EXPECT_FALSE(FilesUtils::isFileInUse(filePath));

    QFile f(filePath);
    EXPECT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Append)); // OK, returns true
    EXPECT_TRUE(FilesUtils::isFileInUse(filePath)); // FAILS, returns false
    f.close();
    EXPECT_FALSE(f.open(QIODevice::WriteOnly | QIODevice::Append| QIODevice::NewOnly)); //OK, returns false
    EXPECT_FALSE(FilesUtils::isFileInUse(filePath)); // SUCCEEDS, returns false
}

This code runs as expected:

File free: "/tmp/qt_temp-zCTbRf/test.txt"
OK, FALSE
OK, TRUE
File free: "/tmp/qt_temp-zCTbRf/test.txt"
NOT TRUE!
OK, FALSE
File free: "/tmp/qt_temp-zCTbRf/test.txt"
OK, FALSE
  • Thanks for your propositions! Unfortunately, it seems that if I have 2 QFile's pointing to the same physical file, and if I open the file with the 1st QFile, then the 2nd QFile still says isOpen == false :-( – Alexandre Oct 09 '20 at 06:46
  • You have a second `QFile` object precisely because you do not use `QIODevice::NewOnly`. If you use it, the file already opened is locked, as you wish. See edit. –  Oct 09 '20 at 14:08
1

On Windows a file is opened for reading and/or writing and a share mode allowing additional reading/writing/deleting. This can create many interesting combinations, for example a file can be open for writing and allow additional open for reading but not writing. Or a file can be open for reading and allowing renames/deletes. See dwShareMode parameter of CreateFile WinAPI.

Unfortunately QFile::open API doesn't support share mode, since Qt is a portable framework and share mode exists only on Windows.

See these links with additional information for possible alternative solutions:

rustyx
  • 80,671
  • 25
  • 200
  • 267