launchApp function

Future<LaunchResult> launchApp(
  1. LaunchInput input, {
  2. void onProgress(
    1. String
    ) = _noop,
})

Launches a Flutter app as a detached background process and waits for the VM service URI to appear in the log.

Progress messages are emitted via onProgress. Warnings are prefixed with "WARNING: " so adapters can route them to the appropriate output channel.

Never throws. All error conditions are represented as sealed result cases.

Implementation

Future<LaunchResult> launchApp(
  LaunchInput input, {
  void Function(String) onProgress = _noop,
}) async {
  try {
    final device = input.device;
    final project = input.project ?? Directory.current.path;
    final flavor = input.flavor;
    final target = input.target;
    final flutterSdk = input.flutterSdk;
    final verbose = input.verbose;
    String? deviceLabel;

    if (device == null) return const LaunchMissingDevice();
    onProgress('launch: preparing session');

    // Point all session files at <project>/.fdb/
    initSessionDir(project);

    // Kill any previous controller.
    final oldControllerPid = readControllerPid();
    if (oldControllerPid != null && isProcessAlive(oldControllerPid)) {
      try {
        Process.killPid(oldControllerPid, ProcessSignal.sigterm);
      } catch (_) {}
    }

    // Kill any previous log collector.
    final oldCollectorPid = readLogCollectorPid();
    if (oldCollectorPid != null && isProcessAlive(oldCollectorPid)) {
      try {
        Process.killPid(oldCollectorPid, ProcessSignal.sigterm);
      } catch (_) {}
    }

    // Clean up previous state.
    cleanupLaunchSessionFiles();

    // Create .fdb/ session directory and persist device ID.
    ensureSessionDir();
    _ensureGitignored(project);
    File(deviceFile).writeAsStringSync(device);

    // Resolve the flutter binary: explicit --flutter-sdk, FVM auto-detect, or PATH.
    final flutter = resolveFlutterBinary(
      project,
      explicitSdk: flutterSdk,
      onWarning: onProgress,
    );

    // Resolve and persist the target platform + emulator flag for this device.
    // Used by `fdb screenshot` to dispatch to the correct capture backend.
    // Non-fatal: screenshot falls back to the old heuristic if this fails.
    deviceLabel = await writePlatformInfoForLaunch(device, flutter);

    // Persist the app bundle id / package name for later use by crash-report.
    // Non-fatal: crash-report will ask the user for --app-id if this fails.
    writeAppIdFromProjectForLaunch(project);

    final controllerLaunch = _resolveControllerLaunch();

    final controllerArgs = [
      ...controllerLaunch.arguments,
      '--session-dir',
      ensureSessionDir(),
      '--project',
      project,
      '--device',
      device,
      '--flutter',
      flutter,
      if (flavor != null) ...['--flavor', flavor],
      if (target != null) ...['--target', target],
      if (verbose) '--verbose',
    ];

    late final Process controllerProcess;
    try {
      onProgress('launch: starting controller');
      controllerProcess = await Process.start(
        controllerLaunch.executable,
        controllerArgs,
        mode: ProcessStartMode.detached,
      );
    } on ProcessException catch (e) {
      return LaunchLauncherFailed(e.toString());
    }
    File(controllerPidFile).writeAsStringSync(controllerProcess.pid.toString());

    // Guard against Ctrl-C / SIGTERM during the poll loop: kill the controller
    // so it does not linger as an orphan after the fdb process exits.
    final sigintSub = ProcessSignal.sigint.watch().listen((_) {
      try {
        Process.killPid(controllerProcess.pid, ProcessSignal.sigterm);
      } catch (_) {}
      exit(1);
    });
    final sigtermSub = ProcessSignal.sigterm.watch().listen((_) {
      try {
        Process.killPid(controllerProcess.pid, ProcessSignal.sigterm);
      } catch (_) {}
      exit(1);
    });

    // Poll log file for VM service URI.
    final stopwatch = Stopwatch()..start();
    var lastHeartbeat = 0;
    var reportedLogLines = 0;
    String? vmUri;
    onProgress('launch: starting Flutter on ${deviceLabel ?? 'device $device'}');

    try {
      while (stopwatch.elapsed.inSeconds < launchTimeoutSeconds) {
        await Future<void>.delayed(const Duration(milliseconds: pollIntervalMs));

        // Heartbeat so the caller knows we're not stuck.
        final elapsedSeconds = stopwatch.elapsed.inSeconds;
        if (elapsedSeconds ~/ heartbeatIntervalSeconds > lastHeartbeat) {
          lastHeartbeat = elapsedSeconds ~/ heartbeatIntervalSeconds;
          onProgress(
            'launch: still waiting for VM service (${elapsedSeconds}s elapsed)',
          );
        }

        // Check if the controller process died unexpectedly.
        if (!_isAlive(controllerProcess.pid)) {
          final logExists = File(logFile).existsSync();
          if (logExists) {
            final logContent = File(logFile).readAsStringSync();
            return LaunchProcessDied(fullLog: logContent);
          } else {
            return const LaunchProcessDied(noLogFile: true);
          }
        }

        if (!File(logFile).existsSync()) continue;

        final lines = File(logFile).readAsLinesSync();
        if (lines.length > reportedLogLines) {
          for (final line in lines.skip(reportedLogLines)) {
            final progress = _progressFromLogLine(line);
            if (progress != null) {
              onProgress(progress);
            }
          }
          reportedLogLines = lines.length;
        }

        vmUri = readVmUri();
        if (vmUri != null && vmUri.isNotEmpty) break;
      }

      if (vmUri == null) {
        final tailLogLines = <String>[];
        if (File(logFile).existsSync()) {
          final lines = File(logFile).readAsLinesSync();
          tailLogLines.addAll(
            lines.length > 10 ? lines.sublist(lines.length - 10) : lines,
          );
        }
        return LaunchTimeout(tailLogLines: tailLogLines);
      }

      final pid = readLaunchPid();

      return LaunchSuccess(
        vmServiceUri: vmUri,
        pid: pid,
        logFilePath: logFile,
      );
    } finally {
      await sigintSub.cancel();
      await sigtermSub.cancel();
    }
  } catch (e) {
    return LaunchError(e.toString());
  }
}