error_handling_io.dart 25.6 KB
Newer Older
1 2 3 4 5
// Copyright 2014 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:convert';
6
import 'dart:io' as io show Directory, File, Link, Process, ProcessException, ProcessResult, ProcessSignal, ProcessStartMode, systemEncoding;
7
import 'dart:typed_data';
8 9 10

import 'package:file/file.dart';
import 'package:meta/meta.dart';
11
import 'package:path/path.dart' as p; // flutter_ignore: package_path_import
12
import 'package:process/process.dart';
13 14

import 'common.dart' show throwToolExit;
15
import 'platform.dart';
16

17
// The Flutter tool hits file system and process errors that only the end-user can address.
18 19 20 21 22 23
// We would like these errors to not hit crash logging. In these cases, we
// should exit gracefully and provide potentially useful advice. For example, if
// a write fails because the target device is full, we can explain that with a
// ToolExit and a message that is more clear than the FileSystemException by
// itself.

24 25 26 27
/// On windows this is error code 2: ERROR_FILE_NOT_FOUND, and on
/// macOS/Linux it is error code 2/ENOENT: No such file or directory.
const int kSystemCannotFindFile = 2;

28 29 30 31 32 33 34 35
/// A [FileSystem] that throws a [ToolExit] on certain errors.
///
/// If a [FileSystem] error is not caused by the Flutter tool, and can only be
/// addressed by the user, it should be caught by this [FileSystem] and thrown
/// as a [ToolExit] using [throwToolExit].
///
/// Cf. If there is some hope that the tool can continue when an operation fails
/// with an error, then that error/operation should not be handled here. For
36
/// example, the tool should generally be able to continue executing even if it
37 38
/// fails to delete a file.
class ErrorHandlingFileSystem extends ForwardingFileSystem {
39
  ErrorHandlingFileSystem({
40 41
    required FileSystem delegate,
    required Platform platform,
42 43 44 45 46
  }) :
      assert(delegate != null),
      assert(platform != null),
      _platform = platform,
      super(delegate);
47 48 49 50

  @visibleForTesting
  FileSystem get fileSystem => delegate;

51 52
  final Platform _platform;

53 54 55 56 57 58 59 60
  /// Allow any file system operations executed within the closure to fail with any
  /// operating system error, rethrowing an [Exception] instead of a [ToolExit].
  ///
  /// This should not be used with async file system operation.
  ///
  /// This can be used to bypass the [ErrorHandlingFileSystem] permission exit
  /// checks for situations where failure is acceptable, such as the flutter
  /// persistent settings cache.
61
  static void noExitOnFailure(void Function() operation) {
62 63 64
    final bool previousValue = ErrorHandlingFileSystem._noExitOnFailure;
    try {
      ErrorHandlingFileSystem._noExitOnFailure = true;
65
      operation();
66 67 68 69 70
    } finally {
      ErrorHandlingFileSystem._noExitOnFailure = previousValue;
    }
  }

71 72 73
  /// Delete the file or directory and return true if it exists, take no
  /// action and return false if it does not.
  ///
74
  /// This method should be preferred to checking if it exists and
75 76 77 78 79 80 81 82 83 84 85 86
  /// then deleting, because it handles the edge case where the file or directory
  /// is deleted by a different program between the two calls.
  static bool deleteIfExists(FileSystemEntity file, {bool recursive = false}) {
    if (!file.existsSync()) {
      return false;
    }
    try {
      file.deleteSync(recursive: recursive);
    } on FileSystemException catch (err) {
      // Certain error codes indicate the file could not be found. It could have
      // been deleted by a different program while the tool was running.
      // if it still exists, the file likely exists on a read-only volume.
87
      if (err.osError?.errorCode != kSystemCannotFindFile || _noExitOnFailure) {
88 89 90 91 92
        rethrow;
      }
      if (file.existsSync()) {
        throwToolExit(
          'The Flutter tool tried to delete the file or directory ${file.path} but was '
93
          "unable to. This may be due to the file and/or project's location on a read-only "
94 95 96 97 98 99 100
          'volume. Consider relocating the project and trying again',
        );
      }
    }
    return true;
  }

101 102
  static bool _noExitOnFailure = false;

103
  @override
104
  Directory get currentDirectory {
105 106 107 108
    try {
      return _runSync(() =>  directory(delegate.currentDirectory), platform: _platform);
    } on FileSystemException catch (err) {
      // Special handling for OS error 2 for current directory only.
109
      if (err.osError?.errorCode == kSystemCannotFindFile) {
110 111 112 113 114 115 116
        throwToolExit(
          'Unable to read current working directory. This can happen if the directory the '
          'Flutter tool was run from was moved or deleted.'
        );
      }
      rethrow;
    }
117
  }
118

119 120 121 122 123 124 125 126 127 128 129 130 131 132
  @override
  File file(dynamic path) => ErrorHandlingFile(
    platform: _platform,
    fileSystem: delegate,
    delegate: delegate.file(path),
  );

  @override
  Directory directory(dynamic path) => ErrorHandlingDirectory(
    platform: _platform,
    fileSystem: delegate,
    delegate: delegate.directory(path),
  );

133
  @override
134 135 136 137 138
  Link link(dynamic path) => ErrorHandlingLink(
    platform: _platform,
    fileSystem: delegate,
    delegate: delegate.link(path),
  );
139 140 141 142 143 144 145 146

  // Caching the path context here and clearing when the currentDirectory setter
  // is updated works since the flutter tool restricts usage of dart:io directly
  // via the forbidden import tests. Otherwise, the path context's current
  // working directory might get out of sync, leading to unexpected results from
  // methods like `path.relative`.
  @override
  p.Context get path => _cachedPath ??= delegate.path;
147
  p.Context? _cachedPath;
148 149 150 151 152 153

  @override
  set currentDirectory(dynamic path) {
    _cachedPath = null;
    delegate.currentDirectory = path;
  }
154 155 156

  @override
  String toString() => delegate.toString();
157 158 159 160 161
}

class ErrorHandlingFile
    extends ForwardingFileSystemEntity<File, io.File>
    with ForwardingFile {
162
  ErrorHandlingFile({
163 164 165
    required Platform platform,
    required this.fileSystem,
    required this.delegate,
166 167 168 169 170
  }) :
    assert(platform != null),
    assert(fileSystem != null),
    assert(delegate != null),
    _platform = platform;
171 172 173 174 175 176 177

  @override
  final io.File delegate;

  @override
  final FileSystem fileSystem;

178 179
  final Platform _platform;

180
  @override
181 182 183 184 185
  File wrapFile(io.File delegate) => ErrorHandlingFile(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
186 187

  @override
188 189 190 191 192
  Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
193 194

  @override
195 196 197 198 199
  Link wrapLink(io.Link delegate) => ErrorHandlingLink(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
200 201 202 203 204 205 206 207 208 209 210 211 212

  @override
  Future<File> writeAsBytes(
    List<int> bytes, {
    FileMode mode = FileMode.write,
    bool flush = false,
  }) async {
    return _run<File>(
      () async => wrap(await delegate.writeAsBytes(
        bytes,
        mode: mode,
        flush: flush,
      )),
213
      platform: _platform,
214
      failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
215
      posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
216 217 218
    );
  }

219 220 221 222 223 224
  @override
  String readAsStringSync({Encoding encoding = utf8}) {
    return _runSync<String>(
      () => delegate.readAsStringSync(),
      platform: _platform,
      failureMessage: 'Flutter failed to read a file at "${delegate.path}"',
225
      posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
226 227 228
    );
  }

229 230 231 232 233 234 235 236
  @override
  void writeAsBytesSync(
    List<int> bytes, {
    FileMode mode = FileMode.write,
    bool flush = false,
  }) {
    _runSync<void>(
      () => delegate.writeAsBytesSync(bytes, mode: mode, flush: flush),
237
      platform: _platform,
238
      failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
239
      posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
    );
  }

  @override
  Future<File> writeAsString(
    String contents, {
    FileMode mode = FileMode.write,
    Encoding encoding = utf8,
    bool flush = false,
  }) async {
    return _run<File>(
      () async => wrap(await delegate.writeAsString(
        contents,
        mode: mode,
        encoding: encoding,
        flush: flush,
      )),
257
      platform: _platform,
258
      failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
259
      posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
    );
  }

  @override
  void writeAsStringSync(
    String contents, {
    FileMode mode = FileMode.write,
    Encoding encoding = utf8,
    bool flush = false,
  }) {
    _runSync<void>(
      () => delegate.writeAsStringSync(
        contents,
        mode: mode,
        encoding: encoding,
        flush: flush,
      ),
277
      platform: _platform,
278
      failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
279
      posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
280 281 282
    );
  }

283
  // TODO(aam): Pass `exclusive` through after dartbug.com/49647 lands.
284
  @override
285
  void createSync({bool recursive = false, bool exclusive = false}) {
286 287 288 289 290 291
    _runSync<void>(
      () => delegate.createSync(
        recursive: recursive,
      ),
      platform: _platform,
      failureMessage: 'Flutter failed to create file at "${delegate.path}"',
292
      posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(<String>[delegate.parent.path]),
293 294 295
    );
  }

296 297 298 299 300 301 302 303
  @override
  RandomAccessFile openSync({FileMode mode = FileMode.read}) {
    return _runSync<RandomAccessFile>(
      () => delegate.openSync(
        mode: mode,
      ),
      platform: _platform,
      failureMessage: 'Flutter failed to open a file at "${delegate.path}"',
304
      posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
305 306 307
    );
  }

308 309 310 311 312 313 314 315
  /// This copy method attempts to handle file system errors from both reading
  /// and writing the copied file.
  @override
  File copySync(String newPath) {
    final File resultFile = fileSystem.file(newPath);
    // First check if the source file can be read. If not, bail through error
    // handling.
    _runSync<void>(
316
      () => delegate.openSync().closeSync(),
317
      platform: _platform,
318 319
      failureMessage: 'Flutter failed to copy $path to $newPath due to source location error',
      posixPermissionSuggestion: _posixPermissionSuggestion(<String>[path]),
320 321 322 323
    );
    // Next check if the destination file can be written. If not, bail through
    // error handling.
    _runSync<void>(
324
      () => resultFile.createSync(recursive: true),
325
      platform: _platform,
Pierre-Louis's avatar
Pierre-Louis committed
326
      failureMessage: 'Flutter failed to copy $path to $newPath due to destination location error'
327 328 329 330 331 332 333 334 335 336 337
    );
    // If both of the above checks passed, attempt to copy the file and catch
    // any thrown errors.
    try {
      return wrapFile(delegate.copySync(newPath));
    } on FileSystemException {
      // Proceed below
    }
    // If the copy failed but both of the above checks passed, copy the bytes
    // directly.
    _runSync(() {
338 339
      RandomAccessFile? source;
      RandomAccessFile? sink;
340
      try {
341
        source = delegate.openSync();
342 343 344 345 346 347 348 349 350 351
        sink = resultFile.openSync(mode: FileMode.writeOnly);
        // 64k is the same sized buffer used by dart:io for `File.openRead`.
        final Uint8List buffer = Uint8List(64 * 1024);
        final int totalBytes = source.lengthSync();
        int bytes = 0;
        while (bytes < totalBytes) {
          final int chunkLength = source.readIntoSync(buffer);
          sink.writeFromSync(buffer, 0, chunkLength);
          bytes += chunkLength;
        }
352
      } catch (err) { // ignore: avoid_catches_without_on_clauses, rethrows
353 354 355 356 357 358
        ErrorHandlingFileSystem.deleteIfExists(resultFile, recursive: true);
        rethrow;
      } finally {
        source?.closeSync();
        sink?.closeSync();
      }
359 360 361 362
    }, platform: _platform,
      failureMessage: 'Flutter failed to copy $path to $newPath due to unknown error',
      posixPermissionSuggestion: _posixPermissionSuggestion(<String>[path, resultFile.parent.path]),
    );
363
    // The original copy failed, but the manual copy worked.
364 365 366
    return wrapFile(resultFile);
  }

367 368 369
  String _posixPermissionSuggestion(List<String> paths) => 'Try running:\n'
      '  sudo chown -R \$(whoami) ${paths.map(fileSystem.path.absolute).join(' ')}';

370 371
  @override
  String toString() => delegate.toString();
372 373 374 375 376
}

class ErrorHandlingDirectory
    extends ForwardingFileSystemEntity<Directory, io.Directory>
    with ForwardingDirectory<Directory> {
377
  ErrorHandlingDirectory({
378 379 380
    required Platform platform,
    required this.fileSystem,
    required this.delegate,
381 382 383 384 385
  }) :
    assert(platform != null),
    assert(fileSystem != null),
    assert(delegate != null),
    _platform = platform;
386 387 388 389 390 391 392

  @override
  final io.Directory delegate;

  @override
  final FileSystem fileSystem;

393 394
  final Platform _platform;

395
  @override
396 397 398 399 400
  File wrapFile(io.File delegate) => ErrorHandlingFile(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
401 402

  @override
403 404 405 406 407
  Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
408 409

  @override
410 411 412 413 414
  Link wrapLink(io.Link delegate) => ErrorHandlingLink(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429

  // For the childEntity methods, we first obtain an instance of the entity
  // from the underlying file system, then invoke childEntity() on it, then
  // wrap in the ErrorHandling version.
  @override
  Directory childDirectory(String basename) =>
    wrapDirectory(fileSystem.directory(delegate).childDirectory(basename));

  @override
  File childFile(String basename) =>
    wrapFile(fileSystem.directory(delegate).childFile(basename));

  @override
  Link childLink(String basename) =>
    wrapLink(fileSystem.directory(delegate).childLink(basename));
430

431 432 433 434 435 436 437
  @override
  void createSync({bool recursive = false}) {
    return _runSync<void>(
      () => delegate.createSync(recursive: recursive),
      platform: _platform,
      failureMessage:
        'Flutter failed to create a directory at "${delegate.path}"',
438
      posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.parent.path),
439 440 441
    );
  }

442
  @override
443
  Future<Directory> createTemp([String? prefix]) {
444 445 446 447 448 449 450 451 452
    return _run<Directory>(
      () async => wrap(await delegate.createTemp(prefix)),
      platform: _platform,
      failureMessage:
        'Flutter failed to create a temporary directory with prefix "$prefix"',
    );
  }

  @override
453
  Directory createTempSync([String? prefix]) {
454 455 456 457 458 459 460 461
    return _runSync<Directory>(
      () => wrap(delegate.createTempSync(prefix)),
      platform: _platform,
      failureMessage:
        'Flutter failed to create a temporary directory with prefix "$prefix"',
    );
  }

462 463 464 465 466 467 468
  @override
  Future<Directory> create({bool recursive = false}) {
    return _run<Directory>(
      () async => wrap(await delegate.create(recursive: recursive)),
      platform: _platform,
      failureMessage:
        'Flutter failed to create a directory at "${delegate.path}"',
469
      posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.parent.path),
470 471 472 473 474 475 476 477 478 479
    );
  }

  @override
  Future<Directory> delete({bool recursive = false}) {
    return _run<Directory>(
      () async => wrap(fileSystem.directory((await delegate.delete(recursive: recursive)).path)),
      platform: _platform,
      failureMessage:
        'Flutter failed to delete a directory at "${delegate.path}"',
480
      posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.path),
481 482 483 484 485 486 487 488 489 490
    );
  }

  @override
  void deleteSync({bool recursive = false}) {
    return _runSync<void>(
      () => delegate.deleteSync(recursive: recursive),
      platform: _platform,
      failureMessage:
        'Flutter failed to delete a directory at "${delegate.path}"',
491
      posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.path),
492 493 494
    );
  }

495 496 497 498 499 500 501
  @override
  bool existsSync() {
    return _runSync<bool>(
      () => delegate.existsSync(),
      platform: _platform,
      failureMessage:
        'Flutter failed to check for directory existence at "${delegate.path}"',
502
      posixPermissionSuggestion: _posixPermissionSuggestion(delegate.parent.path),
503 504 505
    );
  }

506 507 508
  String _posixPermissionSuggestion(String path) => 'Try running:\n'
      '  sudo chown -R \$(whoami) ${fileSystem.path.absolute(path)}';

509 510
  @override
  String toString() => delegate.toString();
511 512 513 514 515
}

class ErrorHandlingLink
    extends ForwardingFileSystemEntity<Link, io.Link>
    with ForwardingLink {
516
  ErrorHandlingLink({
517 518 519
    required Platform platform,
    required this.fileSystem,
    required this.delegate,
520 521 522 523 524
  }) :
    assert(platform != null),
    assert(fileSystem != null),
    assert(delegate != null),
    _platform = platform;
525 526 527 528 529 530 531

  @override
  final io.Link delegate;

  @override
  final FileSystem fileSystem;

532 533
  final Platform _platform;

534
  @override
535 536 537 538 539
  File wrapFile(io.File delegate) => ErrorHandlingFile(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
540 541

  @override
542 543 544 545 546
  Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
547 548

  @override
549 550 551 552 553
  Link wrapLink(io.Link delegate) => ErrorHandlingLink(
    platform: _platform,
    fileSystem: fileSystem,
    delegate: delegate,
  );
554 555 556

  @override
  String toString() => delegate.toString();
557
}
558

559 560
const String _kNoExecutableFound = 'The Flutter tool could not locate an executable with suitable permissions';

561
Future<T> _run<T>(Future<T> Function() op, {
562 563
  required Platform platform,
  String? failureMessage,
564
  String? posixPermissionSuggestion,
565 566 567 568
}) async {
  assert(platform != null);
  try {
    return await op();
569 570 571 572 573
  } on ProcessPackageExecutableNotFoundException catch (e) {
    if (e.candidates.isNotEmpty) {
      throwToolExit('$_kNoExecutableFound: $e');
    }
    rethrow;
574 575
  } on FileSystemException catch (e) {
    if (platform.isWindows) {
576
      _handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0);
577
    } else if (platform.isLinux || platform.isMacOS) {
578
      _handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0, posixPermissionSuggestion);
579 580 581 582
    }
    rethrow;
  } on io.ProcessException catch (e) {
    if (platform.isWindows) {
583
      _handleWindowsException(e, failureMessage, e.errorCode);
584
    } else if (platform.isLinux || platform.isMacOS) {
585
      _handlePosixException(e, failureMessage, e.errorCode, posixPermissionSuggestion);
586 587 588 589 590 591
    }
    rethrow;
  }
}

T _runSync<T>(T Function() op, {
592 593
  required Platform platform,
  String? failureMessage,
594
  String? posixPermissionSuggestion,
595 596 597 598
}) {
  assert(platform != null);
  try {
    return op();
599 600 601 602 603
  } on ProcessPackageExecutableNotFoundException catch (e) {
    if (e.candidates.isNotEmpty) {
      throwToolExit('$_kNoExecutableFound: $e');
    }
    rethrow;
604 605
  } on FileSystemException catch (e) {
    if (platform.isWindows) {
606
      _handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0);
607
    } else if (platform.isLinux || platform.isMacOS) {
608
      _handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0, posixPermissionSuggestion);
609 610 611 612
    }
    rethrow;
  } on io.ProcessException catch (e) {
    if (platform.isWindows) {
613
      _handleWindowsException(e, failureMessage, e.errorCode);
614
    } else if (platform.isLinux || platform.isMacOS) {
615
      _handlePosixException(e, failureMessage, e.errorCode, posixPermissionSuggestion);
616 617 618 619 620
    }
    rethrow;
  }
}

621

622 623 624 625 626 627 628
/// A [ProcessManager] that throws a [ToolExit] on certain errors.
///
/// If a [ProcessException] is not caused by the Flutter tool, and can only be
/// addressed by the user, it should be caught by this [ProcessManager] and thrown
/// as a [ToolExit] using [throwToolExit].
///
/// See also:
629
///   * [ErrorHandlingFileSystem], for a similar file system strategy.
630
class ErrorHandlingProcessManager extends ProcessManager {
631
  ErrorHandlingProcessManager({
632 633
    required ProcessManager delegate,
    required Platform platform,
634 635
  }) : _delegate = delegate,
       _platform = platform;
636

637 638
  final ProcessManager _delegate;
  final Platform _platform;
639 640

  @override
641
  bool canRun(dynamic executable, {String? workingDirectory}) {
642 643
    return _runSync(
      () => _delegate.canRun(executable, workingDirectory: workingDirectory),
644
      platform: _platform,
645 646 647
      failureMessage: 'Flutter failed to run "$executable"',
      posixPermissionSuggestion: 'Try running:\n'
          '  sudo chown -R \$(whoami) $executable && chmod u+rx $executable',
648 649 650 651
    );
  }

  @override
652
  bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
653 654
    return _runSync(
      () => _delegate.killPid(pid, signal),
655 656 657 658 659 660
      platform: _platform,
    );
  }

  @override
  Future<io.ProcessResult> run(
661 662 663
    List<Object> command, {
    String? workingDirectory,
    Map<String, String>? environment,
664 665
    bool includeParentEnvironment = true,
    bool runInShell = false,
666 667
    Encoding? stdoutEncoding = io.systemEncoding,
    Encoding? stderrEncoding = io.systemEncoding,
668
  }) {
669 670 671 672 673 674 675 676 677 678 679
    return _run(() {
      return _delegate.run(
        command,
        workingDirectory: workingDirectory,
        environment: environment,
        includeParentEnvironment: includeParentEnvironment,
        runInShell: runInShell,
        stdoutEncoding: stdoutEncoding,
        stderrEncoding: stderrEncoding,
      );
    }, platform: _platform);
680 681 682 683
  }

  @override
  Future<io.Process> start(
684 685 686
    List<Object> command, {
    String? workingDirectory,
    Map<String, String>? environment,
687 688 689 690
    bool includeParentEnvironment = true,
    bool runInShell = false,
    io.ProcessStartMode mode = io.ProcessStartMode.normal,
  }) {
691 692 693 694 695 696 697 698 699
    return _run(() {
      return _delegate.start(
        command,
        workingDirectory: workingDirectory,
        environment: environment,
        includeParentEnvironment: includeParentEnvironment,
        runInShell: runInShell,
      );
    }, platform: _platform);
700 701 702 703
  }

  @override
  io.ProcessResult runSync(
704 705 706
    List<Object> command, {
    String? workingDirectory,
    Map<String, String>? environment,
707 708
    bool includeParentEnvironment = true,
    bool runInShell = false,
709 710
    Encoding? stdoutEncoding = io.systemEncoding,
    Encoding? stderrEncoding = io.systemEncoding,
711
  }) {
712 713 714 715 716 717 718 719 720 721 722
    return _runSync(() {
      return _delegate.runSync(
        command,
        workingDirectory: workingDirectory,
        environment: environment,
        includeParentEnvironment: includeParentEnvironment,
        runInShell: runInShell,
        stdoutEncoding: stdoutEncoding,
        stderrEncoding: stderrEncoding,
      );
    }, platform: _platform);
723 724 725
  }
}

726
void _handlePosixException(Exception e, String? message, int errorCode, String? posixPermissionSuggestion) {
727 728 729
  // From:
  // https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h
  // https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
730
  // https://github.com/apple/darwin-xnu/blob/master/bsd/dev/dtrace/scripts/errno.d
731
  const int eperm = 1;
732
  const int enospc = 28;
733
  const int eacces = 13;
734
  // Catch errors and bail when:
735
  String? errorMessage;
736 737
  switch (errorCode) {
    case enospc:
738
      errorMessage =
739 740
        '$message. The target device is full.'
        '\n$e\n'
741
        'Free up space and try again.';
742
      break;
743
    case eperm:
744
    case eacces:
745 746 747 748 749 750 751 752 753 754 755 756
      final StringBuffer errorBuffer = StringBuffer();
      if (message != null && message.isNotEmpty) {
        errorBuffer.writeln('$message.');
      } else {
        errorBuffer.writeln('The flutter tool cannot access the file or directory.');
      }
      errorBuffer.writeln('Please ensure that the SDK and/or project is installed in a location '
          'that has read/write permissions for the current user.');
      if (posixPermissionSuggestion != null && posixPermissionSuggestion.isNotEmpty) {
        errorBuffer.writeln(posixPermissionSuggestion);
      }
      errorMessage = errorBuffer.toString();
757
      break;
758 759 760 761
    default:
      // Caller must rethrow the exception.
      break;
  }
762
  _throwFileSystemException(errorMessage);
763 764
}

765
void _handleWindowsException(Exception e, String? message, int errorCode) {
766 767 768 769
  // From:
  // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes
  const int kDeviceFull = 112;
  const int kUserMappedSectionOpened = 1224;
770
  const int kAccessDenied = 5;
771
  const int kFatalDeviceHardwareError = 483;
772
  const int kDeviceDoesNotExist = 433;
773

774
  // Catch errors and bail when:
775
  String? errorMessage;
776
  switch (errorCode) {
777
    case kAccessDenied:
778
      errorMessage =
779
        '$message. The flutter tool cannot access the file or directory.\n'
780
        'Please ensure that the SDK and/or project is installed in a location '
781
        'that has read/write permissions for the current user.';
782
      break;
783
    case kDeviceFull:
784
      errorMessage =
785 786
        '$message. The target device is full.'
        '\n$e\n'
787
        'Free up space and try again.';
788 789
      break;
    case kUserMappedSectionOpened:
790
      errorMessage =
791 792 793
        '$message. The file is being used by another program.'
        '\n$e\n'
        'Do you have an antivirus program running? '
794
        'Try disabling your antivirus program and try again.';
795
      break;
796 797 798 799 800
    case kFatalDeviceHardwareError:
      errorMessage =
        '$message. There is a problem with the device driver '
        'that this file or directory is stored on.';
      break;
801 802 803 804 805 806
    case kDeviceDoesNotExist:
      errorMessage =
        '$message. The device was not found.'
        '\n$e\n'
        'Verify the device is mounted and try again.';
      break;
807 808 809 810
    default:
      // Caller must rethrow the exception.
      break;
  }
811 812 813
  _throwFileSystemException(errorMessage);
}

814
void _throwFileSystemException(String? errorMessage) {
815 816 817 818 819 820 821
  if (errorMessage == null) {
    return;
  }
  if (ErrorHandlingFileSystem._noExitOnFailure) {
    throw Exception(errorMessage);
  }
  throwToolExit(errorMessage);
822
}