attachApp function
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;
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);
}
// Resolve the debug URL: use the explicit flag when provided; otherwise
// try to auto-discover it from device logs (Android logcat, iOS unified
// log, or idevicesyslog on physical devices).
String? debugUrl;
if (input.debugUrl != null) {
debugUrl = normalizeAttachDebugUrl(input.debugUrl!);
} else {
final platformInfo = readPlatformInfo();
if (platformInfo != null) {
onProgress('attach: attempting VM service URI auto-discovery');
// Physical iOS log collection (idevicesyslog archive) typically takes
// 10–15 s; use a 30 s timeout so the archive has time to complete.
final isPhysicalIos =
(platformInfo.platform == 'ios' || platformInfo.platform.startsWith('ios-')) && !platformInfo.emulator;
final discoveryTimeout = isPhysicalIos ? const Duration(seconds: 30) : const Duration(seconds: 5);
debugUrl = await discoverVmServiceUrl(
device: device,
platformInfo: platformInfo,
timeout: discoveryTimeout,
onProgress: onProgress,
);
if (debugUrl != null) {
onProgress('attach: discovered VM service URI — $debugUrl');
} else {
onProgress('attach: VM service URI not found in device logs; falling back to mDNS discovery');
}
}
}
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());
}
}