replacePreviousSession function

Future<void> replacePreviousSession({
  1. bool stopApp = false,
  2. String progressPrefix = 'launch',
  3. void onProgress(
    1. String projectPath
    ) = _noop,
})

Stops what the previous session in the current session dir left behind and removes its files, so a new launch / attach can start cleanly.

A dead session (see isPreviousSessionStale) has its leftover controller, flutter tool and log collector processes terminated and waited for, and a single WARNING: line is reported through onProgress. A live session is replaced: with stopApp its app is first stopped through the controller (the app.stop path fdb kill uses), then its controller and log collector receive SIGTERM.

On SIGTERM the controller stops its flutter run / flutter attach child before exiting (on mobile targets the app keeps running, verified on the iOS simulator; desktop and web are not verified), so it gets controllerShutdownTimeout before SIGKILL. fdb launch passes stopApp because it starts the app again anyway; fdb attach does not, as it may be attaching to the very app the previous session ran.

Stopping a hung session can take up to about 30 s, so progress lines prefixed with progressPrefix (launch / attach) are reported through onProgress before each wait.

Implementation

Future<void> replacePreviousSession({
  bool stopApp = false,
  String progressPrefix = 'launch',
  void Function(String) onProgress = _noop,
}) async {
  if (await isPreviousSessionStale()) {
    final controllerPid = readControllerPid();
    if (controllerPid != null) {
      if (isProcessAlive(controllerPid)) onProgress('$progressPrefix: stopping previous session');
      await terminateProcess(controllerPid, timeout: controllerShutdownTimeout);
    }
    final pids = {readPid(), readLogCollectorPid()}.whereType<int>().where((pid) => pid != controllerPid);
    for (final pid in pids) {
      await terminateProcess(pid, timeout: const Duration(seconds: 3));
    }
    cleanupLaunchSessionFiles();
    onProgress('WARNING: Cleaned up a stale session (previous app or controller was no longer running)');
    return;
  }

  onProgress('$progressPrefix: stopping previous session');
  if (stopApp) {
    try {
      await sendControllerCommand(ControllerCommand.kill, timeout: const Duration(seconds: killTimeoutSeconds));
    } catch (_) {
      // Best effort: the controller is signalled below either way.
    }
  }

  final logCollectorPid = readLogCollectorPid();
  if (logCollectorPid != null && isProcessAlive(logCollectorPid)) {
    try {
      Process.killPid(logCollectorPid, ProcessSignal.sigterm);
    } catch (_) {}
  }
  final controllerPid = readControllerPid();
  if (controllerPid != null && isProcessAlive(controllerPid)) {
    onProgress('$progressPrefix: waiting for the previous session to exit');
    await terminateProcess(controllerPid, timeout: controllerShutdownTimeout);
  }
  cleanupLaunchSessionFiles();
}