attachApp function

Future<AttachResult> attachApp(
  1. AttachInput input, {
  2. void onProgress(
    1. String
    ) = _noop,
})

Attaches fdb to an already-running Flutter app and waits for the VM service.

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

Implementation

Future<AttachResult> attachApp(
  AttachInput input, {
  void Function(String) onProgress = _noop,
}) async {
  try {
    final device = input.device;
    final project = input.project ?? Directory.current.path;
    final flutterSdk = input.flutterSdk;
    final appId = input.appId;
    final debugUrl = input.debugUrl == null ? null : normalizeAttachDebugUrl(input.debugUrl!);
    String? deviceLabel;

    if (device == null) return const AttachMissingDevice();
    onProgress('attach: preparing session');

    initLaunchSession(project: project, sessionDir: input.sessionDir);
    _stopPreviousSessionProcesses();
    cleanupLaunchSessionFiles();

    ensureSessionDir();
    ensureGitignored(project);
    File(deviceFile).writeAsStringSync(device);

    final flutter = resolveFlutterBinary(
      project,
      explicitSdk: flutterSdk,
      onWarning: onProgress,
    );

    deviceLabel = await writePlatformInfoForLaunch(device, flutter);
    writeAppIdFromProjectForLaunch(project, flavor: input.flavor);
    if (appId != null && appId.isNotEmpty) {
      writeAppId(appId);
    }

    final controllerLaunch = resolveControllerLaunch();
    final controllerArgs = buildAttachControllerArgs(
      controllerLaunch.arguments,
      sessionDir: ensureSessionDir(),
      project: project,
      device: device,
      flutter: flutter,
      target: input.target,
      appId: appId,
      debugUrl: debugUrl,
      verbose: input.verbose,
    );

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

    final sigintSub = ProcessSignal.sigint.watch().listen((_) {
      _killPid(controllerProcess.pid);
      exit(1);
    });
    final sigtermSub = ProcessSignal.sigterm.watch().listen((_) {
      _killPid(controllerProcess.pid);
      exit(1);
    });

    final stopwatch = Stopwatch()..start();
    var lastHeartbeat = 0;
    var reportedLogLines = 0;
    String? vmUri;
    onProgress('attach: waiting for Flutter app on ${deviceLabel ?? 'device $device'}');

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

        final elapsedSeconds = stopwatch.elapsed.inSeconds;
        if (elapsedSeconds ~/ heartbeatIntervalSeconds > lastHeartbeat) {
          lastHeartbeat = elapsedSeconds ~/ heartbeatIntervalSeconds;
          onProgress(
            'attach: still waiting for VM service (${elapsedSeconds}s elapsed)',
          );
        }

        if (!isProcessAlive(controllerProcess.pid)) {
          final logExists = File(logFile).existsSync();
          if (logExists) {
            final logContent = File(logFile).readAsStringSync();
            return AttachProcessDied(fullLog: logContent);
          }
          return const AttachProcessDied(noLogFile: true);
        }

        if (File(logFile).existsSync()) {
          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 AttachTimeout(tailLogLines: tailLogLines);
      }

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