isolate_current_directory

GitHub Actions pub package

This library exports a single function, withCurrentDirectory, which can change Directory.current (the working directory) within the scope of a lambda, but not the global value.

That means that using this function, it's possible to write concurrent Dart code that executes in different working directories without different computations affecting each other.

Using this library

To add a dependency on your pubspec:

dart pub add isolate_current_directory

Now, you can use withCurrentDirectory:

withCurrentDirectory('my-dir', () async {
  // this file resolves to my-dir/example.txt
  final file = File('example.txt');
  // use the file!
});

See isolate_current_directory_example.dart for a complete example.

Motivation

Dart's Directory.current is a global variable that can be changed at any time by any Dart code.

In asynchronous code, you could use a lock (see the synchronized package) to try to avoid modifying the working directory while other async code is running, but that is impossible to guarantee as any code that ignores the lock could still concurrently modify the working directory.

This problem is even more vexing in the presence of Isolates because if any code in any Isolate changes the working directory, then all other Isolates will see that but have no way that I know of to synchronize access to Directory.current, because Isolates are supposed to be, well, isolated from each other so they cannot share the same lock!

By using IOOverrides, this library leverages Dart Zones to isolate Directory.current to the scope of a function. No matter how many functions are running concurrently, even across many Isolates, each function has its own working directory. It can change its own working directory without affecting any other code running in a different scope.

Caveats

Process

Unfortunately, methods from Process do not honour the scoped Directory.current value by default.

For this reason, when using Process, you must pass in the workingDirectory argument explicitly:

Process.start('cmd', const ['args'], workingDirectory: Directory.current.path);

Isolates

When starting a new Isolate, unfortunately, the new Isolate will not inherit the scoped current directory of the calling code.

To work around that, wrap Isolate functions as follows:

// BEFORE
Isolate.run(myIsolateFunction);

// AFTER
// capture the working directory in this Isolate
final workingDir = Directory.current.path;
// in the new Isolate, use withCurrentDirectory
Isolate.run(() => withCurrentDirectory(workingDir, myIsolateFunction));  

Performance

Another possible issue is performance. When a FileSystemEntity is created within the scope of withCurrentDirectory, a custom implementation of the dart:io type (File, Directory, Link) is created which will check at each operation what's the scoped value of Directory.current, which may have a non-negligible cost if this happens in the hot path of an application.

Links mostly work, but mysteriously, exists() doesn't seem to.

See link_test.dart.

Libraries

isolate_current_directory
Dart's Directory.current can be set to change the current directory, but this is a true global variable shared even between different Isolates.