launchApp function
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());
}
}