| // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'dart:io' show FileLock, Platform, pid; | |
| import 'package:file/file.dart'; | |
| import 'package:logging/logging.dart'; | |
| Future<T> runUnderDirectoriesLock<T>( | |
| FileSystem fileSystem, | |
| List<Uri> directories, | |
| Future<T> Function() callback, { | |
| Duration? timeout, | |
| Logger? logger, | |
| }) async { | |
| if (directories.isEmpty) { | |
| return await callback(); | |
| } | |
| return await runUnderDirectoryLock( | |
| fileSystem, | |
| directories.first, | |
| () => runUnderDirectoriesLock<T>( | |
| fileSystem, | |
| directories.skip(1).toList(), | |
| callback, | |
| timeout: timeout, | |
| logger: logger, | |
| ), | |
| timeout: timeout, | |
| logger: logger, | |
| ); | |
| } | |
| /// Run [callback] with this Dart process having exclusive access to | |
| /// [directory]. | |
| /// | |
| /// Note multiple isolates and isolate groups in the same Dart process share | |
| /// locks, so these will be able to enter the exclusive section simultanously. | |
| /// | |
| /// If provided, the [timeout] parameter determines how long to retry before | |
| /// giving up. If the [timeout] is exceeded a [TimeoutException] is thrown. | |
| /// | |
| /// If provided, the [logger] streams information on the the locking status, and | |
| /// also streams error messages. | |
| Future<T> runUnderDirectoryLock<T>( | |
| FileSystem fileSystem, | |
| Uri directory, | |
| Future<T> Function() callback, { | |
| Duration? timeout, | |
| Logger? logger, | |
| }) async { | |
| const lockFileName = '.lock'; | |
| final lockFile = _fileInDir(fileSystem.directory(directory), lockFileName); | |
| return _runUnderFileLock( | |
| lockFile, | |
| callback, | |
| timeout: timeout, | |
| logger: logger, | |
| ); | |
| } | |
| File _fileInDir(Directory path, String filename) { | |
| final dirPath = path.path; | |
| var separator = Platform.pathSeparator; | |
| if (dirPath.endsWith(separator)) separator = ''; | |
| return path.fileSystem.file('$dirPath$separator$filename'); | |
| } | |
| /// Run [callback] with this Dart process having exclusive access to [file]. | |
| /// | |
| /// Note multiple isolates and isolate groups in the same Dart process share | |
| /// locks, so these will be able to enter the exclusive section simultanously. | |
| /// | |
| /// If provided, the [timeout] parameter determines how long to retry before | |
| /// giving up. If the [timeout] is exceeded a [TimeoutException] is thrown. | |
| /// | |
| /// If provided, the [logger] streams information on the the locking status, | |
| /// and also streams error messages. | |
| Future<T> _runUnderFileLock<T>( | |
| File file, | |
| Future<T> Function() callback, { | |
| Duration? timeout, | |
| Logger? logger, | |
| }) async { | |
| if (!await file.exists()) await file.create(recursive: true); | |
| final randomAccessFile = await file.open(mode: .write); | |
| var printed = false; | |
| var errorFromCallback = false; | |
| final stopwatch = Stopwatch()..start(); | |
| while (timeout == null || stopwatch.elapsed < timeout) { | |
| try { | |
| await randomAccessFile.lock(FileLock.exclusive); | |
| try { | |
| await randomAccessFile.writeString( | |
| 'Last acquired by ${Platform.resolvedExecutable} ' | |
| '(pid $pid) running ${Platform.script} on ${DateTime.now()}.', | |
| ); | |
| try { | |
| return await callback(); | |
| } on FileSystemException { | |
| errorFromCallback = true; | |
| rethrow; | |
| } | |
| } finally { | |
| await randomAccessFile.unlock(); | |
| } | |
| } on FileSystemException { | |
| if (errorFromCallback) { | |
| rethrow; | |
| } | |
| if (!printed) { | |
| logger?.finer( | |
| 'Waiting to be able to obtain lock of directory: ${file.path}.', | |
| ); | |
| printed = true; | |
| } | |
| // Don't busy wait, give the CPU some rest. | |
| // Magic constant taken from flutter_tools for startup lock. | |
| await Future<void>.delayed(const Duration(milliseconds: 50)); | |
| } | |
| } | |
| final message = 'Could not acquire the lock to ${file.path}.'; | |
| logger?.severe(message); | |
| throw TimeoutException(message, timeout); | |
| } |