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 (heartbeats and warnings) are emitted via onProgress. Heartbeats are plain tokens (e.g. "WAITING..."); 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;

    if (device == null) return const LaunchMissingDevice();

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

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

    // Clean up previous state.
    for (final path in [
      pidFile,
      appPidFile,
      logFile,
      logCollectorPidFile,
      logCollectorScript,
      vmUriFile,
      launcherScript,
      deviceFile,
      platformFile,
    ]) {
      final f = File(path);
      if (f.existsSync()) f.deleteSync();
    }

    // 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 = _resolveFlutter(project, flutterSdk, 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.
    await _writePlatformInfo(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.
    _writeAppIdFromProject(project, device);

    // Build the flutter run command string.
    final flutterArgs = [
      flutter,
      'run',
      '-d',
      device,
      '--debug',
      '--pid-file',
      pidFile,
      if (flavor != null) ...['--flavor', flavor],
      if (target != null) ...['--target', target],
      if (verbose) '--verbose',
    ];
    final flutterCmd = flutterArgs.map(_shellEscape).join(' ');

    // Write a launcher script that runs flutter in the foreground (nohup keeps
    // it alive after the parent exits, and & backgrounds it from our perspective).
    final script = '''
#!/bin/bash
cd ${_shellEscape(project)}
exec $flutterCmd > $logFile 2>&1
''';
    File(launcherScript).writeAsStringSync(script);
    Process.runSync('chmod', ['+x', launcherScript]);

    // Launch via nohup + & so the process is fully detached from this parent.
    final result = await Process.run('bash', [
      '-c',
      'nohup bash $launcherScript &\necho \$!',
    ]);

    if (result.exitCode != 0) {
      return LaunchLauncherFailed(result.stderr as String);
    }

    final launcherPid = int.tryParse((result.stdout as String).trim());
    if (launcherPid == null) return const LaunchInvalidLauncherPid();

    // Poll log file for VM service URI.
    final stopwatch = Stopwatch()..start();
    var lastHeartbeat = 0;
    String? vmUri;

    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('WAITING...');
      }

      // Check if the launcher process died unexpectedly.
      if (!_isAlive(launcherPid)) {
        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 logContent = File(logFile).readAsStringSync();
      vmUri = _extractVmUri(logContent);
      if (vmUri != null) 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);
    }

    // Save VM service URI.
    File(vmUriFile).writeAsStringSync(vmUri);

    // Read PID — flutter writes it via --pid-file, fall back to launcher PID.
    final pid = File(pidFile).existsSync() ? File(pidFile).readAsStringSync().trim() : launcherPid.toString();

    // Start the log collector — a background process that subscribes to the
    // VM service Logging/Stdout/Stderr streams and appends to the log file.
    // flutter run only forwards print() to stdout; developer.log() events are
    // only available via the VM service, so this fills that gap.
    await _startLogCollector(vmUri, onProgress);

    // Retrieve the app VM PID via getVM and persist it to fdb.app_pid.
    // This is the Dart VM process PID (different from the flutter-tools PID in
    // fdb.pid). Used by vmServiceCall for liveness detection on macOS desktop.
    // Non-fatal: if getVM fails for any reason, fdb.app_pid is simply not written
    // and vmServiceCall falls back to the flutter-tools PID heuristic.
    await _writeAppPid();

    return LaunchSuccess(
      vmServiceUri: vmUri,
      pid: pid,
      logFilePath: logFile,
    );
  } catch (e) {
    return LaunchError(e.toString());
  }
}