defaultScaffoldProcessRunner function

Future<void> defaultScaffoldProcessRunner(
  1. String executable,
  2. List<String> args, {
  3. String? workingDirectory,
  4. Duration? timeout,
})

Implementation

Future<void> defaultScaffoldProcessRunner(
  String executable,
  List<String> args, {
  String? workingDirectory,
  Duration? timeout,
}) async {
  try {
    Process process;
    if (Platform.isWindows) {
      process = await Process.start('cmd', [
        '/c',
        executable,
        ...args,
      ], workingDirectory: workingDirectory);
    } else {
      process = await Process.start(
        executable,
        args,
        workingDirectory: workingDirectory,
      );
    }

    // listen + Completer instead of addStream: allows cancelling
    // subscriptions on timeout to free stdout/stderr for subsequent calls.
    final stdoutDone = Completer<void>();
    final stderrDone = Completer<void>();
    final stdoutSub = process.stdout.listen(
      stdout.add,
      onDone: stdoutDone.complete,
    );
    final stderrSub = process.stderr.listen(
      stderr.add,
      onDone: stderrDone.complete,
    );

    Future<void> drainStreams() =>
        Future.wait([stdoutDone.future, stderrDone.future]);

    void killProcessTree() {
      if (Platform.isWindows) {
        // process.kill only kills cmd.exe, not child processes spawned by
        // flutter.bat — taskkill /T kills the entire process tree.
        try {
          Process.runSync('taskkill', ['/F', '/T', '/PID', '${process.pid}']);
        } catch (e) {
          logger.warn('taskkill failed, falling back to sigkill: $e');
          process.kill(ProcessSignal.sigkill);
        }
      } else {
        process.kill(ProcessSignal.sigkill);
      }
    }

    if (timeout != null) {
      final exitCode = await process.exitCode.timeout(
        timeout,
        onTimeout: () {
          killProcessTree();
          return -1;
        },
      );
      if (exitCode == -1) {
        stdoutSub.cancel();
        stderrSub.cancel();
        throw TimeoutException(
          '$executable ${args.join(' ')} timed out after '
          '${timeout.inSeconds} seconds and was terminated.',
        );
      }
      await drainStreams();
      if (exitCode != 0) {
        throw Exception(
          '$executable ${args.join(' ')} failed with exit code $exitCode',
        );
      }
    } else {
      await drainStreams();
      final exitCode = await process.exitCode;
      if (exitCode != 0) {
        throw Exception(
          '$executable ${args.join(' ')} failed with exit code $exitCode',
        );
      }
    }
  } on ProcessException catch (e) {
    throw Exception(
      'Unable to run $executable. Make sure it is installed and added to PATH.\n'
      'Error: ${e.message}',
    );
  }
}