0

I used File::Find to traverse a directory tree and Win32::File's GetAttributes function to look at the attributes of files found in it. This worked in a single-threaded program.

Then I moved the directory traversal into a separate thread, and it stopped working. GetAttributes failed on every file with "The system cannot find the file specified" as the error message in $^E.

I traced the problem to the fact that File::Find uses chdir, and apparently GetAttributes doesn't use the current directory. I could work around this by passing it an absolute path, but then I could run into path length limits, and long paths are definitely going to be present where this script will run, so I really need to take advantage of chdir and relative paths.

To demonstrate the problem, here is a script which creates a file in the current directory, another file in a subdirectory, chdir's to the subdirectory, and looks for the file 3 ways: system("dir"), open, and GetAttributes.

When the script is run without arguments, dir shows the subdirectory, open finds the file in the subdirectory, and GetAttributes returns its attributes successfully. When run with --thread, all the tests are done in a subthread, and the dir and open still work, but the GetAttributes fails. Then it calls GetAttributes on the file that is in the original directory (which we have chdir'ed out of) and it finds that one! Somehow GetAttributes is using the original working directory of the process - or maybe the working directory of the main thread - unlike all the other file operations.

How can I fix this? I can guarantee that the main thread won't do any chdir'ing, if that matters.

use strict;
use warnings;

use threads;
use Data::Dumper;
use Win32::File qw/GetAttributes/;
sub doit
{
  chdir("testdir") or die "chdir: $!\n";
  system "dir";
  my $attribs;
  open F, '<', "file.txt" or die "open: $!\n";
  print "open succeeded. File contents:\n-------\n", <F>, "\n--------\n";
  close F;
  my $x = GetAttributes("file.txt", $attribs);
  print Dumper [$x, $attribs, $!, $^E];
  if(!$x) {
    # If we didn't find the file we were supposed to find, how about the
    # bad one?
    $x = GetAttributes("badfile.txt", $attribs);
    if($x) {
      print "GetAttributes found the bad file!\n";
      if(open F, '<', "badfile.txt") {
        print "opened the bad file\n";
        close F;
      } else {
        print "But open didn't open it. Error: $! ($^E)\n";
      }
    }
  }
}

# Setup
-d "testdir" or mkdir "testdir" or die "mkdir testdir: $!\n";
if(!-f "badfile.txt") {
  open F, '>', "badfile.txt" or die "create badfile.txt: $!\n";
  print F "bad\n";
  close F;
}
if(!-f "testdir/file.txt") {
  open F, '>', "testdir/file.txt" or die "create testdir/file.txt: $!\n";
  print F "hello\n";
  close F;
}

# Option 1: do it in the main thread - works fine
if(!(@ARGV && $ARGV[0] eq '--thread')) {
  doit();
}

# Option 2: do it in a secondary thread - GetAttributes fails
if(@ARGV && $ARGV[0] eq '--thread') {
  my $thr = threads->create(\&doit);
  $thr->join();
}

1 Answers1

0

Eventually, I figured out that perl is maintaining some kind of secondary cwd that only applies to perl built-in operators, while GetAttributes is using the native cwd. I don't know why it does this or why it only happens in the secondary thread; my best guess is that perl is trying to emulate the unix rule of one cwd per process, and failing because the Win32::* modules don't play along.

Whatever the reason, it's possible to work around it by forcing the native cwd to be the same as perl's cwd whenever you're about to do a Win32::* operation, like this:

use Cwd;
use Win32::FindFile qw/SetCurrentDirectory/;

...

SetCurrentDirectory(getcwd());

Arguably File::Find should do this when running on Win32.

Of course this only makes the "pathname too long" problem worse, because now every directory you visit will be the target of an absolute-path SetCurrentDirectory; try to work around it with a series of smaller SetCurrentDirectory calls and you have to figure out a way to get back where you came from, which is hard when you don't even have fchdir.