Skip to content

[Filesystem] "Text file too busy" when trying to remove directories in a Vagrant shared folder #47804

Closed
@garethellis36

Description

@garethellis36

Symfony version(s) affected

5.4, 6.1

Description

I believe this issue was introduced by #40144, which made a change to doRemove to make copies of directories with temporary names before removing them.

I came across this while attempting to update dependencies in a legacy library which is a dependency of our main application.

When I run the tests for this legacy library, I get this:

> vendor/bin/phpunit
PHPUnit 9.5.25 #StandWithUkraine

Runtime:       PHP 8.1.11
Configuration: /var/other/flex/phpunit.xml

.....E......                                                      12 / 12 (100%)

Time: 00:00.095, Memory: 4.00 MB

There was 1 error:

1) IconLanguageServices\Flex\Test\Integration\FolderArchiverTest::test_it_can_move_files_to_archive_directory
IconLanguageServices\Flex\Exception\CouldNotArchive: Failed to archiveFailed to remove directory "/var/other/flex/test/Integration/TestFileSystem/src/..YzH": rmdir(/var/other/flex/test/Integration/TestFileSystem/src/..YzH): Text file busy

/var/other/flex/src/FolderArchiver.php:58
/var/other/flex/test/Integration/FolderArchiverTest.php:60

Caused by
Symfony\Component\Filesystem\Exception\IOException: Failed to remove directory "/var/other/flex/test/Integration/TestFileSystem/src/..YzH": rmdir(/var/other/flex/test/Integration/TestFileSystem/src/..YzH): Text file busy

/var/other/flex/vendor/symfony/filesystem/Filesystem.php:191
/var/other/flex/vendor/symfony/filesystem/Filesystem.php:150
/var/other/flex/src/FolderArchiver.php:56
/var/other/flex/test/Integration/FolderArchiverTest.php:60

ERRORS!
Tests: 12, Assertions: 15, Errors: 1.

The test which fails looks like this:

<?php

namespace IconLanguageServices\Flex\Test\Integration;

use IconLanguageServices\Flex\FolderArchiver;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

class FolderArchiverTest extends \IconLanguageServices\Flex\Test\TestCase
{
    private Filesystem $fs;
    private string $src;
    private string $tgt;
    private FolderArchiver $archiver;

    public function setUp(): void
    {
        parent::setUp();
        $this->fs = new Filesystem();

        $this->src = __DIR__ . "/TestFileSystem/src/123";
        $this->tgt = __DIR__ . "/TestFileSystem/tgt";

        if (!$this->fs->exists($this->src)) {
            $this->fs->mkdir($this->src);
        }

        if (!$this->fs->exists("{$this->src}/file1")) {
            $this->fs->touch("{$this->src}/file1");
        }

        if (!$this->fs->exists("{$this->src}/file2")) {
            $this->fs->touch("{$this->src}/file2");
        }

        if (!$this->fs->exists($this->tgt)) {
            $this->fs->mkdir($this->tgt);
        }

        $this->archiver = new FolderArchiver($this->fs, new Finder(), $this->tgt);
    }

    public function tearDown(): void
    {
        parent::tearDown();
        if ($this->fs->exists($this->src)) {
            $this->fs->remove($this->src);
        }

        if ($this->fs->exists($this->tgt)) {
            $this->fs->remove($this->tgt);
        }
    }

    /**
     * @return void
     */
    public function test_it_can_move_files_to_archive_directory()
    {
        $this->archiver->archive($this->src);

        $this->assertFalse(
            file_exists($this->src),
            "Source folder has not been moved"
        );

        $date = date("Y-m-d");
        $this->assertTrue(
            file_exists($this->tgt . DIRECTORY_SEPARATOR . $date . DIRECTORY_SEPARATOR . "123"),
            "Folder named '123' not found in archive folder"
        );

        $this->assertTrue(
            file_exists($this->tgt . DIRECTORY_SEPARATOR . $date . DIRECTORY_SEPARATOR . "123" . DIRECTORY_SEPARATOR . "file1"),
            "File named 'file1' not found in archive sub-folder"
        );

        $this->assertTrue(
            file_exists($this->tgt . DIRECTORY_SEPARATOR . $date . DIRECTORY_SEPARATOR . "123" . DIRECTORY_SEPARATOR . "file2"),
            "File named 'file2' not found in archive sub-folder"
        );
    }
}

The subject-under-test looks like this:

<?php

namespace IconLanguageServices\Flex;

use IconLanguageServices\Flex\Exception\CouldNotArchive;
use IconLanguageServices\Flex\Exception\FolderNotFound;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

class FolderArchiver
{
    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * @var string
     */
    private $archiveFolder;
    /**
     * @var Finder
     */
    private $finder;

    public function __construct(Filesystem $filesystem, Finder $finder, $archiveFolder)
    {
        $this->filesystem = $filesystem;
        $this->finder = $finder;

        if (!$filesystem->exists($archiveFolder)) {
            $filesystem->mkdir($archiveFolder);
        }
        $this->archiveFolder = $archiveFolder;
    }

    /**
     * @param string $folder
     * @return void
     * @throws FolderNotFound
     */
    public function archive($folder)
    {
        if (!$this->filesystem->exists($folder)) {
            throw new FolderNotFound($folder . " not found");
        }
        
        try {
            foreach ($this->finder->files()->in($folder) as $file) {
                /** @var $file SplFileInfo */
                $this->filesystem->copy($file, $this->getArchiveDestination($folder) . $file->getFilename());
            }

            $this->filesystem->remove($folder);
        } catch (IOException $e) {
            throw new CouldNotArchive("Failed to archive" . $e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @param $folder
     * @return string
     */
    private function getArchiveDestination($folder)
    {
        $parts = array_filter(preg_split("/[\/\\\\]/", $folder));
        return $this->archiveFolder . DIRECTORY_SEPARATOR . date("Y-m-d") . DIRECTORY_SEPARATOR . array_pop($parts) . DIRECTORY_SEPARATOR;
    }
}

I am running PHP 8.1.1 inside a RHEL VM using Vagrant 2.3.0 and VirtualBox, with Windows 10 as the host OS. All files in the library's dir are shared from a directory in the host OS.

All tests passed in this same environment before I changed the dependency requirements.

I installed PHP 8.1 for Windows in the host OS and ran the tests in cmd.exe, and they all passed. The tests all pass in our CI environment (Jenkins on RHEL) as well. I am therefore confident that this is related to Vagrant.

How to reproduce

Run the PHPUnit test described above, for the SUT described above, on PHP 8.1 inside a Vagrant VM, with the files shared from a host OS.

Possible Solution

No response

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions