Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion lib/src/cli/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,41 @@ class _ProcessOverridesScope extends ProcessOverrides {
/// Abstraction for running commands via command-line.
class _Cmd {
/// Runs the specified [cmd] with the provided [args].
///
/// If [timeout] is provided, the process will be terminated after the
/// specified duration with a [ProcessException].
static Future<ProcessResult> run(
String cmd,
List<String> args, {
required Logger logger,
bool throwOnError = true,
String? workingDirectory,
Duration? timeout,
}) async {
logger.detail('Running: $cmd with $args');
final runProcess = ProcessOverrides.current?.runProcess ?? Process.run;
final result = await runProcess(
final processFuture = runProcess(
cmd,
args,
runInShell: true,
workingDirectory: workingDirectory,
);
final ProcessResult result;
if (timeout != null) {
try {
result = await processFuture.timeout(timeout);
} on TimeoutException {
throw ProcessException(
cmd,
args,
'Timed out after ${timeout.inSeconds} seconds. '
'Check your internet connection.',
-1,
);
}
} else {
result = await processFuture;
}
logger
..detail('stdout:\n${result.stdout}')
..detail('stderr:\n${result.stderr}');
Expand Down
8 changes: 8 additions & 0 deletions lib/src/cli/dart_cli.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
part of 'cli.dart';

/// Default timeout for pub get operations.
///
/// When connected to a network without internet access, pub get can hang
/// indefinitely. This timeout ensures the CLI surfaces a clear error instead.
const _pubGetTimeout = Duration(seconds: 120);

/// Dart CLI
class Dart {
/// Determine whether dart is installed.
Expand All @@ -18,6 +24,7 @@ class Dart {
String cwd = '.',
bool recursive = false,
Set<String> ignore = const {},
Duration timeout = _pubGetTimeout,
}) async {
final initialCwd = cwd;

Expand Down Expand Up @@ -45,6 +52,7 @@ class Dart {
['pub', 'get', '--no-example'],
workingDirectory: cwd,
logger: logger,
timeout: timeout,
);
} finally {
installProgress.complete();
Expand Down
2 changes: 2 additions & 0 deletions lib/src/cli/flutter_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class Flutter {
String cwd = '.',
bool recursive = false,
Set<String> ignore = const {},
Duration timeout = _pubGetTimeout,
}) async {
final initialCwd = cwd;

Expand Down Expand Up @@ -184,6 +185,7 @@ class Flutter {
['pub', 'get', '--no-example'],
workingDirectory: cwd,
logger: logger,
timeout: timeout,
);
} finally {
installProgress.complete();
Expand Down
20 changes: 19 additions & 1 deletion lib/src/commands/create/commands/create_subcommand.dart
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ abstract class CreateSubCommand extends Command<int> {
return result;
}

/// Timeout for the Mason hook's internal `dart pub get` during bootstrapping.
///
/// When connected to a network without internet access, this can hang
/// indefinitely. This timeout ensures the CLI surfaces a clear error instead.
@visibleForTesting
static const preGenTimeout = Duration(seconds: 120);

/// Invoked by [run] to create the project, contains the logic for using
/// the template vars obtained by [getTemplateVars] to generate the project
/// from the [generator] and [template].
Expand All @@ -222,7 +229,18 @@ abstract class CreateSubCommand extends Command<int> {
final generateProgress = logger.progress('Bootstrapping');
final target = DirectoryGeneratorTarget(outputDirectory);

await generator.hooks.preGen(vars: vars, onVarsChanged: (v) => vars = v);
try {
await generator.hooks
.preGen(vars: vars, onVarsChanged: (v) => vars = v)
.timeout(preGenTimeout);
} on TimeoutException {
generateProgress.fail('Timed out waiting for hook dependencies.');
logger.err(
'Bootstrapping timed out after ${preGenTimeout.inSeconds} seconds.\n'
'Check your internet connection and try again.',
);
return ExitCode.unavailable.code;
}
final files = await generator.generate(target, vars: vars, logger: logger);
generateProgress.complete('Generated ${files.length} file(s)');

Expand Down
30 changes: 30 additions & 0 deletions test/src/cli/dart_cli_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:mason/mason.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
Expand Down Expand Up @@ -284,6 +286,34 @@ void main() {
runProcess: process.run,
);
});

test('throws ProcessException when pub get times out', () async {
when(
() => process.run(
'dart',
any(that: contains('pub')),
runInShell: any(named: 'runInShell'),
workingDirectory: any(named: 'workingDirectory'),
),
).thenAnswer((_) => Completer<ProcessResult>().future);

await ProcessOverrides.runZoned(
() => expectLater(
Dart.pubGet(
logger: logger,
timeout: const Duration(milliseconds: 100),
),
throwsA(
isA<ProcessException>().having(
(e) => e.message,
'message',
contains('Timed out'),
),
),
),
runProcess: process.run,
);
});
});

group('.applyFixes', () {
Expand Down
28 changes: 28 additions & 0 deletions test/src/cli/flutter_cli_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,34 @@ void main() {
runProcess: process.run,
);
});

test('throws ProcessException when pub get times out', () async {
when(
() => process.run(
'flutter',
any(that: contains('pub')),
runInShell: any(named: 'runInShell'),
workingDirectory: any(named: 'workingDirectory'),
),
).thenAnswer((_) => Completer<ProcessResult>().future);

await ProcessOverrides.runZoned(
() => expectLater(
Flutter.pubGet(
logger: logger,
timeout: const Duration(milliseconds: 100),
),
throwsA(
isA<ProcessException>().having(
(e) => e.message,
'message',
contains('Timed out'),
),
),
),
runProcess: process.run,
);
});
});
});

Expand Down
41 changes: 41 additions & 0 deletions test/src/commands/create/create_subcommand_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';

import 'package:args/args.dart';
Expand Down Expand Up @@ -587,6 +588,46 @@ See https://dart.dev/tools/pub/pubspec#name for more information.'''),
).called(1);
});
});

test(
'returns unavailable exit code when preGen times out',
() async {
when(
() => hooks.preGen(
vars: any(named: 'vars'),
onVarsChanged: any(named: 'onVarsChanged'),
),
).thenAnswer((_) => Completer<void>().future);

final result = await runner.run([
'create_subcommand',
'test_project',
]);

expect(result, equals(ExitCode.unavailable.code));
verify(() => progress.fail(any())).called(1);
verify(
() => logger.err(
any(
that: contains(
'Bootstrapping timed out after '
'${CreateSubCommand.preGenTimeout.inSeconds} seconds.',
),
),
),
).called(1);
verifyNever(
() => generator.generate(
any(),
vars: any(named: 'vars'),
logger: any(named: 'logger'),
),
);
},
timeout: Timeout(
CreateSubCommand.preGenTimeout + const Duration(seconds: 5),
),
);
});
});
group('OrgName', () {
Expand Down
Loading