blob: ee5eaf8b9f708b681086b7c1c4f8723eb233ddf4 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. 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 'package:file/file.dart';
import 'package:file/local.dart' as local_fs;
import 'package:meta/meta.dart';
import 'common.dart';
import 'io.dart';
import 'logger.dart';
import 'signals.dart';
// package:file/local.dart must not be exported. This exposes LocalFileSystem,
// which we override to ensure that temporary directories are cleaned up when
// the tool is killed by a signal.
export 'package:file/file.dart';
/// Exception indicating that a file that was expected to exist was not found.
class FileNotFoundException implements IOException {
const FileNotFoundException(this.path);
final String path;
@override
String toString() => 'File not found: $path';
}
/// Return a relative path if [fullPath] is contained by the cwd, else return an
/// absolute path.
String getDisplayPath(String fullPath, FileSystem fileSystem) {
final String cwd =
fileSystem.currentDirectory.path + fileSystem.path.separator;
return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath;
}
/// This class extends [local_fs.LocalFileSystem] in order to clean up
/// directories and files that the tool creates under the system temporary
/// directory when the tool exits either normally or when killed by a signal.
class LocalFileSystem extends local_fs.LocalFileSystem {
LocalFileSystem(this._signals, this._fatalSignals, this.shutdownHooks);
@visibleForTesting
LocalFileSystem.test({
required Signals signals,
List<ProcessSignal> fatalSignals = Signals.defaultExitSignals,
}) : this(signals, fatalSignals, ShutdownHooks());
Directory? _systemTemp;
final Map<ProcessSignal, Object> _signalTokens = <ProcessSignal, Object>{};
final ShutdownHooks shutdownHooks;
Future<void> dispose() async {
_tryToDeleteTemp();
for (final MapEntry<ProcessSignal, Object> signalToken
in _signalTokens.entries) {
await _signals.removeHandler(signalToken.key, signalToken.value);
}
_signalTokens.clear();
}
final Signals _signals;
final List<ProcessSignal> _fatalSignals;
void _tryToDeleteTemp() {
try {
if (_systemTemp?.existsSync() ?? false) {
_systemTemp?.deleteSync(recursive: true);
}
} on FileSystemException {
// ignore
}
_systemTemp = null;
}
// This getter returns a fresh entry under /tmp, like
// /tmp/flutter_tools.abcxyz, then the rest of the tool creates /tmp entries
// under that, like /tmp/flutter_tools.abcxyz/flutter_build_stuff.123456.
// Right before exiting because of a signal or otherwise, we delete
// /tmp/flutter_tools.abcxyz, not the whole of /tmp.
@override
Directory get systemTempDirectory {
if (_systemTemp == null) {
if (!superSystemTempDirectory.existsSync()) {
throwToolExit(
'Your system temp directory (${superSystemTempDirectory.path}) does not exist. '
'Did you set an invalid override in your environment? See issue https://github.com/flutter/flutter/issues/74042 for more context.');
}
_systemTemp = superSystemTempDirectory.createTempSync('flutter_tools.')
..createSync(recursive: true);
// Make sure that the temporary directory is cleaned up if the tool is
// killed by a signal.
for (final ProcessSignal signal in _fatalSignals) {
final Object token = _signals.addHandler(
signal,
(ProcessSignal _) {
_tryToDeleteTemp();
},
);
_signalTokens[signal] = token;
}
// Make sure that the temporary directory is cleaned up when the tool
// exits normally.
shutdownHooks.addShutdownHook(
_tryToDeleteTemp,
);
}
return _systemTemp!;
}
// This only exist because the memory file system does not support a systemTemp that does not exists #74042
@visibleForTesting
Directory get superSystemTempDirectory => super.systemTempDirectory;
}
/// A function that will be run before the VM exits.
typedef ShutdownHook = FutureOr<void> Function();
abstract class ShutdownHooks {
factory ShutdownHooks() => _DefaultShutdownHooks();
/// Registers a [ShutdownHook] to be executed before the VM exits.
void addShutdownHook(ShutdownHook shutdownHook);
@visibleForTesting
List<ShutdownHook> get registeredHooks;
/// Runs all registered shutdown hooks and returns a future that completes when
/// all such hooks have finished.
///
/// Shutdown hooks will be run in groups by their [ShutdownStage]. All shutdown
/// hooks within a given stage will be started in parallel and will be
/// guaranteed to run to completion before shutdown hooks in the next stage are
/// started.
///
/// This class is constructed before the [Logger], so it cannot be direct
/// injected in the constructor.
Future<void> runShutdownHooks(Logger logger);
}
class _DefaultShutdownHooks implements ShutdownHooks {
_DefaultShutdownHooks();
@override
final List<ShutdownHook> registeredHooks = <ShutdownHook>[];
bool _shutdownHooksRunning = false;
@override
void addShutdownHook(ShutdownHook shutdownHook) {
assert(!_shutdownHooksRunning);
registeredHooks.add(shutdownHook);
}
@override
Future<void> runShutdownHooks(Logger logger) async {
logger.printTrace(
'Running ${registeredHooks.length} shutdown hook${registeredHooks.length == 1 ? '' : 's'}',
);
_shutdownHooksRunning = true;
try {
final List<Future<dynamic>> futures = <Future<dynamic>>[];
for (final ShutdownHook shutdownHook in registeredHooks) {
final FutureOr<dynamic> result = shutdownHook();
if (result is Future<dynamic>) {
futures.add(result);
}
}
await Future.wait<dynamic>(futures);
} finally {
_shutdownHooksRunning = false;
}
logger.printTrace('Shutdown hooks complete');
}
}